| // Copyright 2014 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise_property.h" |
| |
| #include <memory> |
| |
| #include "base/memory/scoped_refptr.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_function.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_promise.h" |
| #include "third_party/blink/renderer/bindings/core/v8/script_value.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_core.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_binding_for_testing.h" |
| #include "third_party/blink/renderer/bindings/core/v8/v8_gc_controller.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/testing/dummy_page_holder.h" |
| #include "third_party/blink/renderer/core/testing/garbage_collected_script_wrappable.h" |
| #include "third_party/blink/renderer/core/testing/gc_observation.h" |
| #include "third_party/blink/renderer/platform/bindings/dom_wrapper_world.h" |
| #include "third_party/blink/renderer/platform/bindings/script_state.h" |
| #include "third_party/blink/renderer/platform/heap/handle.h" |
| #include "v8/include/v8.h" |
| |
| namespace blink { |
| |
| namespace { |
| |
| class NotReached : public ScriptFunction { |
| public: |
| static v8::Local<v8::Function> CreateFunction(ScriptState* script_state) { |
| NotReached* self = new NotReached(script_state); |
| return self->BindToV8Function(); |
| } |
| |
| private: |
| explicit NotReached(ScriptState* script_state) |
| : ScriptFunction(script_state) {} |
| |
| ScriptValue Call(ScriptValue) override; |
| }; |
| |
| ScriptValue NotReached::Call(ScriptValue) { |
| EXPECT_TRUE(false) << "'Unreachable' code was reached"; |
| return ScriptValue(); |
| } |
| |
| class StubFunction : public ScriptFunction { |
| public: |
| static v8::Local<v8::Function> CreateFunction(ScriptState* script_state, |
| ScriptValue& value, |
| size_t& call_count) { |
| StubFunction* self = new StubFunction(script_state, value, call_count); |
| return self->BindToV8Function(); |
| } |
| |
| private: |
| StubFunction(ScriptState* script_state, |
| ScriptValue& value, |
| size_t& call_count) |
| : ScriptFunction(script_state), value_(value), call_count_(call_count) {} |
| |
| ScriptValue Call(ScriptValue arg) override { |
| value_ = arg; |
| call_count_++; |
| return ScriptValue(); |
| } |
| |
| ScriptValue& value_; |
| size_t& call_count_; |
| }; |
| |
| class GarbageCollectedHolder final : public GarbageCollectedScriptWrappable { |
| public: |
| typedef ScriptPromiseProperty<Member<GarbageCollectedScriptWrappable>, |
| Member<GarbageCollectedScriptWrappable>, |
| Member<GarbageCollectedScriptWrappable>> |
| Property; |
| GarbageCollectedHolder(ExecutionContext* execution_context) |
| : GarbageCollectedScriptWrappable("holder"), |
| property_(new Property(execution_context, |
| ToGarbageCollectedScriptWrappable(), |
| Property::kReady)) {} |
| |
| Property* GetProperty() { return property_; } |
| GarbageCollectedScriptWrappable* ToGarbageCollectedScriptWrappable() { |
| return this; |
| } |
| |
| void Trace(blink::Visitor* visitor) override { |
| visitor->Trace(property_); |
| GarbageCollectedScriptWrappable::Trace(visitor); |
| } |
| |
| private: |
| Member<Property> property_; |
| }; |
| |
| class ScriptPromisePropertyTestBase { |
| public: |
| ScriptPromisePropertyTestBase() |
| : page_(DummyPageHolder::Create(IntSize(1, 1))) { |
| v8::HandleScope handle_scope(GetIsolate()); |
| other_script_state_ = ScriptState::Create( |
| v8::Context::New(GetIsolate()), |
| DOMWrapperWorld::EnsureIsolatedWorld(GetIsolate(), 1)); |
| } |
| |
| virtual ~ScriptPromisePropertyTestBase() { DestroyContext(); } |
| |
| Document& GetDocument() { return page_->GetDocument(); } |
| v8::Isolate* GetIsolate() { return ToIsolate(&GetDocument()); } |
| ScriptState* MainScriptState() { |
| return ToScriptStateForMainWorld(GetDocument().GetFrame()); |
| } |
| DOMWrapperWorld& MainWorld() { return MainScriptState()->World(); } |
| ScriptState* OtherScriptState() { return other_script_state_; } |
| DOMWrapperWorld& OtherWorld() { return other_script_state_->World(); } |
| ScriptState* CurrentScriptState() { |
| return ScriptState::Current(GetIsolate()); |
| } |
| |
| void DestroyContext() { |
| page_.reset(); |
| if (other_script_state_) { |
| other_script_state_->DisposePerContextData(); |
| other_script_state_ = nullptr; |
| } |
| } |
| |
| void Gc() { |
| V8GCController::CollectAllGarbageForTesting( |
| v8::Isolate::GetCurrent(), |
| v8::EmbedderHeapTracer::EmbedderStackState::kEmpty); |
| } |
| |
| v8::Local<v8::Function> NotReached(ScriptState* script_state) { |
| return NotReached::CreateFunction(script_state); |
| } |
| v8::Local<v8::Function> Stub(ScriptState* script_state, |
| ScriptValue& value, |
| size_t& call_count) { |
| return StubFunction::CreateFunction(script_state, value, call_count); |
| } |
| |
| template <typename T> |
| ScriptValue Wrap(DOMWrapperWorld& world, const T& value) { |
| v8::HandleScope handle_scope(GetIsolate()); |
| ScriptState* script_state = |
| ScriptState::From(ToV8Context(&GetDocument(), world)); |
| ScriptState::Scope scope(script_state); |
| return ScriptValue( |
| script_state, |
| ToV8(value, script_state->GetContext()->Global(), GetIsolate())); |
| } |
| |
| private: |
| std::unique_ptr<DummyPageHolder> page_; |
| Persistent<ScriptState> other_script_state_; |
| }; |
| |
| // This is the main test class. |
| // If you want to examine a testcase independent of holder types, place the |
| // test on this class. |
| class ScriptPromisePropertyGarbageCollectedTest |
| : public ScriptPromisePropertyTestBase, |
| public testing::Test { |
| public: |
| typedef GarbageCollectedHolder::Property Property; |
| |
| ScriptPromisePropertyGarbageCollectedTest() |
| : holder_(new GarbageCollectedHolder(&GetDocument())) {} |
| |
| void ClearHolder() { holder_.Clear(); } |
| GarbageCollectedHolder* Holder() { return holder_; } |
| Property* GetProperty() { return holder_->GetProperty(); } |
| ScriptPromise Promise(DOMWrapperWorld& world) { |
| return GetProperty()->Promise(world); |
| } |
| |
| private: |
| Persistent<GarbageCollectedHolder> holder_; |
| }; |
| |
| // Tests that ScriptPromiseProperty works with a non ScriptWrappable resolution |
| // target. |
| class ScriptPromisePropertyNonScriptWrappableResolutionTargetTest |
| : public ScriptPromisePropertyTestBase, |
| public testing::Test { |
| public: |
| template <typename T> |
| void Test(const T& value, const char* expected, const char* file, int line) { |
| typedef ScriptPromiseProperty<Member<GarbageCollectedScriptWrappable>, T, |
| ToV8UndefinedGenerator> |
| Property; |
| Property* property = new Property( |
| &GetDocument(), new GarbageCollectedScriptWrappable("holder"), |
| Property::kReady); |
| size_t n_resolve_calls = 0; |
| ScriptValue actual_value; |
| String actual; |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| property->Promise(DOMWrapperWorld::MainWorld()) |
| .Then(Stub(CurrentScriptState(), actual_value, n_resolve_calls), |
| NotReached(CurrentScriptState())); |
| } |
| property->Resolve(value); |
| v8::MicrotasksScope::PerformCheckpoint(GetIsolate()); |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| actual = ToCoreString(actual_value.V8Value() |
| ->ToString(MainScriptState()->GetContext()) |
| .ToLocalChecked()); |
| } |
| if (expected != actual) { |
| ADD_FAILURE_AT(file, line) |
| << "toV8 returns an incorrect value.\n Actual: " |
| << actual.Utf8().data() << "\nExpected: " << expected; |
| return; |
| } |
| } |
| }; |
| |
| } // namespace |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, |
| Promise_IsStableObjectInMainWorld) { |
| ScriptPromise v = GetProperty()->Promise(DOMWrapperWorld::MainWorld()); |
| ScriptPromise w = GetProperty()->Promise(DOMWrapperWorld::MainWorld()); |
| EXPECT_EQ(v, w); |
| ASSERT_FALSE(v.IsEmpty()); |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| EXPECT_EQ(v.V8Value().As<v8::Object>()->CreationContext(), |
| ToV8Context(&GetDocument(), MainWorld())); |
| } |
| EXPECT_EQ(Property::kPending, GetProperty()->GetState()); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, |
| Promise_IsStableObjectInVariousWorlds) { |
| ScriptPromise u = GetProperty()->Promise(OtherWorld()); |
| ScriptPromise v = GetProperty()->Promise(DOMWrapperWorld::MainWorld()); |
| ScriptPromise w = GetProperty()->Promise(DOMWrapperWorld::MainWorld()); |
| EXPECT_NE(MainScriptState(), OtherScriptState()); |
| EXPECT_NE(&MainWorld(), &OtherWorld()); |
| EXPECT_NE(u, v); |
| EXPECT_EQ(v, w); |
| ASSERT_FALSE(u.IsEmpty()); |
| ASSERT_FALSE(v.IsEmpty()); |
| { |
| ScriptState::Scope scope(OtherScriptState()); |
| EXPECT_EQ(u.V8Value().As<v8::Object>()->CreationContext(), |
| ToV8Context(&GetDocument(), OtherWorld())); |
| } |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| EXPECT_EQ(v.V8Value().As<v8::Object>()->CreationContext(), |
| ToV8Context(&GetDocument(), MainWorld())); |
| } |
| EXPECT_EQ(Property::kPending, GetProperty()->GetState()); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, |
| Promise_IsStableObjectAfterSettling) { |
| ScriptPromise v = Promise(DOMWrapperWorld::MainWorld()); |
| GarbageCollectedScriptWrappable* value = |
| new GarbageCollectedScriptWrappable("value"); |
| |
| GetProperty()->Resolve(value); |
| EXPECT_EQ(Property::kResolved, GetProperty()->GetState()); |
| |
| ScriptPromise w = Promise(DOMWrapperWorld::MainWorld()); |
| EXPECT_EQ(v, w); |
| EXPECT_FALSE(v.IsEmpty()); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, |
| Promise_DoesNotImpedeGarbageCollection) { |
| ScriptValue holder_wrapper = |
| Wrap(MainWorld(), Holder()->ToGarbageCollectedScriptWrappable()); |
| |
| Persistent<GCObservation> observation; |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| observation = |
| GCObservation::Create(Promise(DOMWrapperWorld::MainWorld()).V8Value()); |
| } |
| |
| Gc(); |
| EXPECT_FALSE(observation->wasCollected()); |
| |
| holder_wrapper.Clear(); |
| ClearHolder(); |
| Gc(); |
| EXPECT_TRUE(observation->wasCollected()); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, |
| Resolve_ResolvesScriptPromise) { |
| ScriptPromise promise = GetProperty()->Promise(DOMWrapperWorld::MainWorld()); |
| ScriptPromise other_promise = GetProperty()->Promise(OtherWorld()); |
| ScriptValue actual, other_actual; |
| size_t n_resolve_calls = 0; |
| size_t n_other_resolve_calls = 0; |
| |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| promise.Then(Stub(CurrentScriptState(), actual, n_resolve_calls), |
| NotReached(CurrentScriptState())); |
| } |
| |
| { |
| ScriptState::Scope scope(OtherScriptState()); |
| other_promise.Then( |
| Stub(CurrentScriptState(), other_actual, n_other_resolve_calls), |
| NotReached(CurrentScriptState())); |
| } |
| |
| EXPECT_NE(promise, other_promise); |
| |
| GarbageCollectedScriptWrappable* value = |
| new GarbageCollectedScriptWrappable("value"); |
| GetProperty()->Resolve(value); |
| EXPECT_EQ(Property::kResolved, GetProperty()->GetState()); |
| |
| v8::MicrotasksScope::PerformCheckpoint(GetIsolate()); |
| EXPECT_EQ(1u, n_resolve_calls); |
| EXPECT_EQ(1u, n_other_resolve_calls); |
| EXPECT_EQ(Wrap(MainWorld(), value), actual); |
| EXPECT_NE(actual, other_actual); |
| EXPECT_EQ(Wrap(OtherWorld(), value), other_actual); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, |
| ResolveAndGetPromiseOnOtherWorld) { |
| ScriptPromise promise = GetProperty()->Promise(DOMWrapperWorld::MainWorld()); |
| ScriptPromise other_promise = GetProperty()->Promise(OtherWorld()); |
| ScriptValue actual, other_actual; |
| size_t n_resolve_calls = 0; |
| size_t n_other_resolve_calls = 0; |
| |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| promise.Then(Stub(CurrentScriptState(), actual, n_resolve_calls), |
| NotReached(CurrentScriptState())); |
| } |
| |
| EXPECT_NE(promise, other_promise); |
| GarbageCollectedScriptWrappable* value = |
| new GarbageCollectedScriptWrappable("value"); |
| GetProperty()->Resolve(value); |
| EXPECT_EQ(Property::kResolved, GetProperty()->GetState()); |
| |
| v8::MicrotasksScope::PerformCheckpoint(GetIsolate()); |
| EXPECT_EQ(1u, n_resolve_calls); |
| EXPECT_EQ(0u, n_other_resolve_calls); |
| |
| { |
| ScriptState::Scope scope(OtherScriptState()); |
| other_promise.Then( |
| Stub(CurrentScriptState(), other_actual, n_other_resolve_calls), |
| NotReached(CurrentScriptState())); |
| } |
| |
| v8::MicrotasksScope::PerformCheckpoint(GetIsolate()); |
| EXPECT_EQ(1u, n_resolve_calls); |
| EXPECT_EQ(1u, n_other_resolve_calls); |
| EXPECT_EQ(Wrap(MainWorld(), value), actual); |
| EXPECT_NE(actual, other_actual); |
| EXPECT_EQ(Wrap(OtherWorld(), value), other_actual); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, Reject_RejectsScriptPromise) { |
| GarbageCollectedScriptWrappable* reason = |
| new GarbageCollectedScriptWrappable("reason"); |
| GetProperty()->Reject(reason); |
| EXPECT_EQ(Property::kRejected, GetProperty()->GetState()); |
| |
| ScriptValue actual, other_actual; |
| size_t n_reject_calls = 0; |
| size_t n_other_reject_calls = 0; |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| GetProperty() |
| ->Promise(DOMWrapperWorld::MainWorld()) |
| .Then(NotReached(CurrentScriptState()), |
| Stub(CurrentScriptState(), actual, n_reject_calls)); |
| } |
| |
| { |
| ScriptState::Scope scope(OtherScriptState()); |
| GetProperty() |
| ->Promise(OtherWorld()) |
| .Then(NotReached(CurrentScriptState()), |
| Stub(CurrentScriptState(), other_actual, n_other_reject_calls)); |
| } |
| |
| v8::MicrotasksScope::PerformCheckpoint(GetIsolate()); |
| EXPECT_EQ(1u, n_reject_calls); |
| EXPECT_EQ(Wrap(MainWorld(), reason), actual); |
| EXPECT_EQ(1u, n_other_reject_calls); |
| EXPECT_NE(actual, other_actual); |
| EXPECT_EQ(Wrap(OtherWorld(), reason), other_actual); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, Promise_DeadContext) { |
| GetProperty()->Resolve(new GarbageCollectedScriptWrappable("value")); |
| EXPECT_EQ(Property::kResolved, GetProperty()->GetState()); |
| |
| DestroyContext(); |
| |
| EXPECT_TRUE(GetProperty()->Promise(DOMWrapperWorld::MainWorld()).IsEmpty()); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, Resolve_DeadContext) { |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| GetProperty() |
| ->Promise(DOMWrapperWorld::MainWorld()) |
| .Then(NotReached(CurrentScriptState()), |
| NotReached(CurrentScriptState())); |
| } |
| |
| DestroyContext(); |
| EXPECT_TRUE(!GetProperty()->GetExecutionContext() || |
| GetProperty()->GetExecutionContext()->IsContextDestroyed()); |
| |
| GetProperty()->Resolve(new GarbageCollectedScriptWrappable("value")); |
| EXPECT_EQ(Property::kPending, GetProperty()->GetState()); |
| |
| v8::MicrotasksScope::PerformCheckpoint(v8::Isolate::GetCurrent()); |
| } |
| |
| TEST_F(ScriptPromisePropertyGarbageCollectedTest, Reset) { |
| ScriptState::Scope scope(MainScriptState()); |
| |
| ScriptPromise old_promise, new_promise; |
| ScriptValue old_actual, new_actual; |
| GarbageCollectedScriptWrappable* old_value = |
| new GarbageCollectedScriptWrappable("old"); |
| GarbageCollectedScriptWrappable* new_value = |
| new GarbageCollectedScriptWrappable("new"); |
| size_t n_old_resolve_calls = 0; |
| size_t n_new_reject_calls = 0; |
| |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| GetProperty()->Resolve(old_value); |
| old_promise = GetProperty()->Promise(MainWorld()); |
| old_promise.Then( |
| Stub(CurrentScriptState(), old_actual, n_old_resolve_calls), |
| NotReached(CurrentScriptState())); |
| } |
| |
| GetProperty()->Reset(); |
| |
| { |
| ScriptState::Scope scope(MainScriptState()); |
| new_promise = GetProperty()->Promise(MainWorld()); |
| new_promise.Then( |
| NotReached(CurrentScriptState()), |
| Stub(CurrentScriptState(), new_actual, n_new_reject_calls)); |
| GetProperty()->Reject(new_value); |
| } |
| |
| EXPECT_EQ(0u, n_old_resolve_calls); |
| EXPECT_EQ(0u, n_new_reject_calls); |
| |
| v8::MicrotasksScope::PerformCheckpoint(GetIsolate()); |
| EXPECT_EQ(1u, n_old_resolve_calls); |
| EXPECT_EQ(1u, n_new_reject_calls); |
| EXPECT_NE(old_promise, new_promise); |
| EXPECT_EQ(Wrap(MainWorld(), old_value), old_actual); |
| EXPECT_EQ(Wrap(MainWorld(), new_value), new_actual); |
| EXPECT_NE(old_actual, new_actual); |
| } |
| |
| TEST_F(ScriptPromisePropertyNonScriptWrappableResolutionTargetTest, |
| ResolveWithUndefined) { |
| Test(ToV8UndefinedGenerator(), "undefined", __FILE__, __LINE__); |
| } |
| |
| TEST_F(ScriptPromisePropertyNonScriptWrappableResolutionTargetTest, |
| ResolveWithString) { |
| Test(String("hello"), "hello", __FILE__, __LINE__); |
| } |
| |
| TEST_F(ScriptPromisePropertyNonScriptWrappableResolutionTargetTest, |
| ResolveWithInteger) { |
| Test(-1, "-1", __FILE__, __LINE__); |
| } |
| |
| } // namespace blink |