// Copyright 2016 The Chromium Authors
// 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/modules/csspaint/paint_worklet.h"

#include <memory>
#include "testing/gmock/include/gmock/gmock.h"
#include "testing/gtest/include/gtest/gtest.h"
#include "third_party/blink/renderer/bindings/core/v8/v8_gc_controller.h"
#include "third_party/blink/renderer/bindings/core/v8/worker_or_worklet_script_controller.h"
#include "third_party/blink/renderer/core/css/css_syntax_definition.h"
#include "third_party/blink/renderer/core/css/cssom/prepopulated_computed_style_property_map.h"
#include "third_party/blink/renderer/core/frame/local_dom_window.h"
#include "third_party/blink/renderer/core/frame/local_frame.h"
#include "third_party/blink/renderer/core/layout/layout_object.h"
#include "third_party/blink/renderer/core/layout/layout_view.h"
#include "third_party/blink/renderer/core/script/classic_script.h"
#include "third_party/blink/renderer/core/testing/page_test_base.h"
#include "third_party/blink/renderer/modules/csspaint/css_paint_definition.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet_global_scope.h"
#include "third_party/blink/renderer/modules/csspaint/paint_worklet_global_scope_proxy.h"
#include "third_party/blink/renderer/platform/graphics/paint_generated_image.h"
#include "third_party/blink/renderer/platform/testing/runtime_enabled_features_test_helpers.h"

namespace blink {
class TestPaintWorklet : public PaintWorklet {
 public:
  explicit TestPaintWorklet(LocalDOMWindow& window) : PaintWorklet(window) {
    ResetIsPaintOffThreadForTesting();
  }

  void SetPaintsToSwitch(int num) { paints_to_switch_ = num; }

  int GetPaintsBeforeSwitching() override { return paints_to_switch_; }

  // We always switch to another global scope so that we can tell how often it
  // was switched in the test.
  wtf_size_t SelectNewGlobalScope() override {
    return (GetActiveGlobalScopeForTesting() + 1) %
           PaintWorklet::kNumGlobalScopesPerThread;
  }

  size_t GetActiveGlobalScope() { return GetActiveGlobalScopeForTesting(); }

 private:
  int paints_to_switch_;
};

class PaintWorkletTest : public PageTestBase {
 public:
  void SetUp() override {
    PageTestBase::SetUp(gfx::Size());
    test_paint_worklet_ =
        MakeGarbageCollected<TestPaintWorklet>(*GetDocument().domWindow());
    proxy_ = test_paint_worklet_->CreateGlobalScope();
  }

  TestPaintWorklet* GetTestPaintWorklet() { return test_paint_worklet_.Get(); }

  size_t SelectGlobalScope(TestPaintWorklet* paint_worklet) {
    return paint_worklet->SelectGlobalScope();
  }

  PaintWorkletGlobalScopeProxy* GetProxy() {
    return PaintWorkletGlobalScopeProxy::From(proxy_.Get());
  }

  ImageResourceObserver* GetImageResourceObserver() {
    return GetDocument().domWindow()->GetFrame()->ContentLayoutObject();
  }

  // Helper function used in GlobalScopeSelection test.
  void ExpectSwitchGlobalScope(bool expect_switch_within_frame,
                               size_t num_paint_calls,
                               int paint_cnt_to_switch,
                               size_t expected_num_paints_before_switch,
                               TestPaintWorklet* paint_worklet_to_test) {
    paint_worklet_to_test->DomWindow()
        ->GetFrame()
        ->View()
        ->UpdateAllLifecyclePhasesForTest();
    paint_worklet_to_test->SetPaintsToSwitch(paint_cnt_to_switch);
    size_t previously_selected_global_scope =
        paint_worklet_to_test->GetActiveGlobalScope();
    size_t global_scope_switch_count = 0u;

    // How many paint calls are there before we switch to another global scope.
    // Because the first paint call in each frame doesn't count as switching,
    // a result of 0 means there is not switching in that frame.
    size_t num_paints_before_switch = 0u;
    for (size_t j = 0; j < num_paint_calls; j++) {
      size_t selected_global_scope = SelectGlobalScope(paint_worklet_to_test);
      if (j == 0) {
        EXPECT_NE(selected_global_scope, previously_selected_global_scope);
      } else if (selected_global_scope != previously_selected_global_scope) {
        num_paints_before_switch = j + 1;
        global_scope_switch_count++;
      }
      previously_selected_global_scope = selected_global_scope;
    }
    EXPECT_LT(global_scope_switch_count, 2u);
    EXPECT_EQ(num_paints_before_switch, expected_num_paints_before_switch);
  }

  void TearDown() override {
    proxy_->TerminateWorkletGlobalScope();
    proxy_ = nullptr;
    PageTestBase::TearDown();
  }

 private:
  Persistent<WorkletGlobalScopeProxy> proxy_;
  Persistent<TestPaintWorklet> test_paint_worklet_;
};

// This is a crash test for crbug.com/803026. At some point, we shipped the
// CSSPaintAPI without shipping the CSSPaintAPIArguments, the result of it is
// that the |paint_arguments| in the CSSPaintDefinition::Paint() becomes
// nullptr and the renderer crashes. This is a regression test to ensure that
// we will never crash.
TEST_F(PaintWorkletTest, PaintWithNullPaintArguments) {
  PaintWorkletGlobalScope* global_scope = GetProxy()->global_scope();
  ClassicScript::CreateUnspecifiedScript(
      "registerPaint('foo', class { paint() { } });")
      ->RunScriptOnScriptState(
          global_scope->ScriptController()->GetScriptState());

  CSSPaintDefinition* definition = global_scope->FindDefinition("foo");
  ASSERT_TRUE(definition);

  ImageResourceObserver* observer = GetImageResourceObserver();
  ASSERT_TRUE(observer);

  const gfx::SizeF container_size(100, 100);
  const LayoutObject& layout_object =
      static_cast<const LayoutObject&>(*observer);
  float zoom = layout_object.StyleRef().EffectiveZoom();
  StylePropertyMapReadOnly* style_map =
      MakeGarbageCollected<PrepopulatedComputedStylePropertyMap>(
          layout_object.GetDocument(), layout_object.StyleRef(),
          definition->NativeInvalidationProperties(),
          definition->CustomInvalidationProperties());
  scoped_refptr<Image> image = PaintGeneratedImage::Create(
      definition->Paint(container_size, zoom, style_map, nullptr),
      container_size);
  EXPECT_NE(image, nullptr);
}

// In this test, we have only one global scope, which means registerPaint is
// called only once, and hence we have only one document paint definition
// registered. In the real world, this document paint definition should not be
// used to paint until we see a second one being registed with the same name.
TEST_F(PaintWorkletTest, SinglyRegisteredDocumentDefinitionNotUsed) {
  PaintWorklet* paint_worklet_to_test =
      PaintWorklet::From(*GetFrame().GetDocument()->domWindow());
  paint_worklet_to_test->ResetIsPaintOffThreadForTesting();

  PaintWorkletGlobalScope* global_scope = GetProxy()->global_scope();
  ClassicScript::CreateUnspecifiedScript(
      "registerPaint('foo', class { paint() { } });")
      ->RunScriptOnScriptState(
          global_scope->ScriptController()->GetScriptState());

  CSSPaintImageGeneratorImpl* generator =
      static_cast<CSSPaintImageGeneratorImpl*>(
          CSSPaintImageGeneratorImpl::Create("foo", GetDocument(), nullptr));
  EXPECT_TRUE(generator);
  EXPECT_FALSE(generator->IsImageGeneratorReady());
  EXPECT_EQ(generator->GetRegisteredDefinitionCountForTesting(), 1u);
  DocumentPaintDefinition* definition;
  // Please refer to CSSPaintImageGeneratorImpl::GetValidDocumentDefinition for
  // the logic.
  EXPECT_TRUE(generator->GetValidDocumentDefinitionForTesting(definition));
}

// In this test, we set a list of "paints_to_switch" numbers, and in each frame,
// we switch to a new global scope when the number of paint calls is >= the
// corresponding number.
TEST_F(PaintWorkletTest, GlobalScopeSelection) {
  TestPaintWorklet* paint_worklet_to_test = GetTestPaintWorklet();

  ExpectSwitchGlobalScope(false, 5, 1, 0, paint_worklet_to_test);
  ExpectSwitchGlobalScope(true, 15, 10, 10, paint_worklet_to_test);
  // In the last one where |paints_to_switch| is 20, there is no switching after
  // the first paint call.
  ExpectSwitchGlobalScope(false, 10, 20, 0, paint_worklet_to_test);
}

TEST_F(PaintWorkletTest, NativeAndCustomProperties) {
  Vector<CSSPropertyID> native_invalidation_properties = {
      CSSPropertyID::kColor,
      CSSPropertyID::kZoom,
      CSSPropertyID::kTop,
  };
  Vector<AtomicString> custom_invalidation_properties = {
      AtomicString("--my-property"),
      AtomicString("--another-property"),
  };

  TestPaintWorklet* paint_worklet_to_test = GetTestPaintWorklet();
  paint_worklet_to_test->RegisterMainThreadDocumentPaintDefinition(
      "foo", native_invalidation_properties, custom_invalidation_properties,
      Vector<CSSSyntaxDefinition>(), true);

  CSSPaintImageGeneratorImpl* generator =
      MakeGarbageCollected<CSSPaintImageGeneratorImpl>(paint_worklet_to_test,
                                                       "foo");
  EXPECT_NE(generator, nullptr);
  EXPECT_EQ(generator->NativeInvalidationProperties().size(), 3u);
  EXPECT_EQ(generator->CustomInvalidationProperties().size(), 2u);
  EXPECT_TRUE(generator->HasAlpha());
}

class MainOrOffThreadPaintWorkletTest
    : public PageTestBase,
      public ::testing::WithParamInterface<bool> {
 public:
  MainOrOffThreadPaintWorkletTest() = default;
  bool IsOffMain() { return GetParam(); }
};

INSTANTIATE_TEST_SUITE_P(All,
                         MainOrOffThreadPaintWorkletTest,
                         ::testing::Bool());

class MockObserver final : public CSSPaintImageGenerator::Observer {
 public:
  MOCK_METHOD0(PaintImageGeneratorReady, void());
};

TEST_P(MainOrOffThreadPaintWorkletTest, ConsistentGlobalScopeOnMainThread) {
  PaintWorklet* paint_worklet_to_test =
      PaintWorklet::From(*GetFrame().GetDocument()->domWindow());
  paint_worklet_to_test->ResetIsPaintOffThreadForTesting(IsOffMain());

  MockObserver* observer = MakeGarbageCollected<MockObserver>();
  CSSPaintImageGeneratorImpl* generator_foo =
      MakeGarbageCollected<CSSPaintImageGeneratorImpl>(
          observer, paint_worklet_to_test, "foo");
  paint_worklet_to_test->AddPendingGenerator("foo", generator_foo);
  CSSPaintImageGeneratorImpl* generator_bar =
      MakeGarbageCollected<CSSPaintImageGeneratorImpl>(
          observer, paint_worklet_to_test, "bar");
  paint_worklet_to_test->AddPendingGenerator("bar", generator_bar);

  // The generator should not fire unless it is the second registration
  // for the main thread case
  EXPECT_CALL(*observer, PaintImageGeneratorReady).Times(0);

  Vector<Persistent<PaintWorkletGlobalScope>> global_scopes;
  for (wtf_size_t i = 0; i < PaintWorklet::kNumGlobalScopesPerThread; ++i) {
    paint_worklet_to_test->AddGlobalScopeForTesting();
    global_scopes.push_back(
        PaintWorkletGlobalScopeProxy::From(
            paint_worklet_to_test->GetGlobalScopesForTesting()[i])
            ->global_scope());
  }

  String foo0 = R"JS(registerPaint('foo', class {
        static get inputProperties() { return ['--foo0']; }
        paint() {}
      });)JS";
  String foo1 = R"JS(registerPaint('foo', class {
        static get inputProperties() { return ['--foo1']; }
        paint() {}
      });)JS";
  String bar = R"JS(registerPaint('bar', class {
        static get inputProperties() { return ['--bar']; }
        paint() {}
      });)JS";

  ClassicScript::CreateUnspecifiedScript(foo0)->RunScriptOnScriptState(
      global_scopes[0]->ScriptController()->GetScriptState());

  EXPECT_TRUE(global_scopes[0]->FindDefinition("foo"));
  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("foo"));

  ClassicScript::CreateUnspecifiedScript(foo1)->RunScriptOnScriptState(
      global_scopes[1]->ScriptController()->GetScriptState());

  // foo0 and foo1 have the same name but different definitions, therefore
  // this definition must become invalid.
  EXPECT_FALSE(paint_worklet_to_test->GetDocumentDefinitionMap().at("foo"));
  DocumentPaintDefinition* invalid_definition = nullptr;
  EXPECT_FALSE(
      generator_foo->GetValidDocumentDefinitionForTesting(invalid_definition));
  EXPECT_FALSE(invalid_definition);
  EXPECT_FALSE(generator_foo->IsImageGeneratorReady());

  ClassicScript::CreateUnspecifiedScript(bar)->RunScriptOnScriptState(
      global_scopes[0]->ScriptController()->GetScriptState());

  EXPECT_TRUE(global_scopes[0]->FindDefinition("bar"));
  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("bar"));

  // When running in main-thread mode, the generator is now ready after this
  // call. For off-thread, we are still waiting on the cross-thread
  // registration.
  if (!IsOffMain()) {
    EXPECT_CALL(*observer, PaintImageGeneratorReady).Times(1);
  }

  ClassicScript::CreateUnspecifiedScript(bar)->RunScriptOnScriptState(
      global_scopes[1]->ScriptController()->GetScriptState());

  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("bar"));
}

// TODO(crbug.com/1430318): All/MainOrOffThreadPaintWorkletTest.
// AllGlobalScopesMustBeCreated/1 is failing on Linux TSan Tests.
#if defined(THREAD_SANITIZER)
#define MAYBE_AllGlobalScopesMustBeCreated DISABLED_AllGlobalScopesMustBeCreated
#else
#define MAYBE_AllGlobalScopesMustBeCreated AllGlobalScopesMustBeCreated
#endif
TEST_P(MainOrOffThreadPaintWorkletTest, MAYBE_AllGlobalScopesMustBeCreated) {
  PaintWorklet* paint_worklet_to_test =
      MakeGarbageCollected<PaintWorklet>(*GetFrame().DomWindow());
  paint_worklet_to_test->ResetIsPaintOffThreadForTesting(IsOffMain());

  EXPECT_TRUE(paint_worklet_to_test->GetGlobalScopesForTesting().empty());

  std::unique_ptr<PaintWorkletPaintDispatcher> dispatcher =
      std::make_unique<PaintWorkletPaintDispatcher>();
  Persistent<PaintWorkletProxyClient> proxy_client =
      MakeGarbageCollected<PaintWorkletProxyClient>(
          1, paint_worklet_to_test,
          GetFrame().GetTaskRunner(TaskType::kInternalDefault),
          dispatcher->GetWeakPtr(), nullptr);
  paint_worklet_to_test->SetProxyClientForTesting(proxy_client);

  while (paint_worklet_to_test->NeedsToCreateGlobalScopeForTesting()) {
    paint_worklet_to_test->AddGlobalScopeForTesting();
  }

  if (IsOffMain()) {
    EXPECT_EQ(paint_worklet_to_test->GetGlobalScopesForTesting().size(),
              2 * PaintWorklet::kNumGlobalScopesPerThread);
  } else {
    EXPECT_EQ(paint_worklet_to_test->GetGlobalScopesForTesting().size(),
              PaintWorklet::kNumGlobalScopesPerThread);
  }
}

TEST_F(PaintWorkletTest, ConsistentGlobalScopeCrossThread) {
  PaintWorklet* paint_worklet_to_test =
      PaintWorklet::From(*GetFrame().GetDocument()->domWindow());
  paint_worklet_to_test->ResetIsPaintOffThreadForTesting(true);

  MockObserver* observer = MakeGarbageCollected<MockObserver>();
  CSSPaintImageGeneratorImpl* generator_foo =
      MakeGarbageCollected<CSSPaintImageGeneratorImpl>(
          observer, paint_worklet_to_test, "foo");
  paint_worklet_to_test->AddPendingGenerator("foo", generator_foo);
  CSSPaintImageGeneratorImpl* generator_bar =
      MakeGarbageCollected<CSSPaintImageGeneratorImpl>(
          observer, paint_worklet_to_test, "bar");
  paint_worklet_to_test->AddPendingGenerator("bar", generator_bar);
  CSSPaintImageGeneratorImpl* generator_loo =
      MakeGarbageCollected<CSSPaintImageGeneratorImpl>(
          observer, paint_worklet_to_test, "loo");
  paint_worklet_to_test->AddPendingGenerator("loo", generator_loo);
  CSSPaintImageGeneratorImpl* generator_gar =
      MakeGarbageCollected<CSSPaintImageGeneratorImpl>(
          observer, paint_worklet_to_test, "gar");
  paint_worklet_to_test->AddPendingGenerator("gar", generator_gar);

  // None of the situations covered in this test should cause the generator to
  // fire.
  EXPECT_CALL(*observer, PaintImageGeneratorReady).Times(0);

  Vector<Persistent<PaintWorkletGlobalScope>> global_scopes;
  for (wtf_size_t i = 0; i < PaintWorklet::kNumGlobalScopesPerThread; ++i) {
    paint_worklet_to_test->AddGlobalScopeForTesting();
    global_scopes.push_back(
        PaintWorkletGlobalScopeProxy::From(
            paint_worklet_to_test->GetGlobalScopesForTesting()[i])
            ->global_scope());
  }

  String foo0 = R"JS(registerPaint('foo', class {
        static get inputProperties() { return ['--foo0']; }
        paint() {}
      });)JS";
  String foo1 = R"JS(registerPaint('foo', class {
        static get inputProperties() { return ['--foo1']; }
        paint() {}
      });)JS";
  String bar0 = R"JS(registerPaint('bar', class {
        static get inputProperties() { return ['--bar0']; }
        paint() {}
      });)JS";
  String loo0 = R"JS(registerPaint('loo', class {
        static get inputProperties() { return ['--loo0']; }
        paint() {}
      });)JS";
  String loo1 = R"JS(registerPaint('loo', class {
        static get inputProperties() { return ['--loo1']; }
        paint() {}
      });)JS";
  String gar0 = R"JS(registerPaint('gar', class {
        static get inputProperties() { return ['--gar0']; }
        paint() {}
      });)JS";

  // Definition invalidated before cross thread check
  ClassicScript::CreateUnspecifiedScript(foo0)->RunScriptOnScriptState(
      global_scopes[0]->ScriptController()->GetScriptState());

  EXPECT_TRUE(global_scopes[0]->FindDefinition("foo"));
  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("foo"));

  ClassicScript::CreateUnspecifiedScript(foo1)->RunScriptOnScriptState(
      global_scopes[1]->ScriptController()->GetScriptState());

  EXPECT_FALSE(paint_worklet_to_test->GetDocumentDefinitionMap().at("foo"));

  CSSPaintDefinition* definition = global_scopes[0]->FindDefinition("foo");

  paint_worklet_to_test->RegisterMainThreadDocumentPaintDefinition(
      "foo", definition->NativeInvalidationProperties(),
      definition->CustomInvalidationProperties(),
      definition->InputArgumentTypes(),
      definition->GetPaintRenderingContext2DSettings()->alpha());

  EXPECT_FALSE(paint_worklet_to_test->GetDocumentDefinitionMap().at("foo"));

  // Definition invalidated by cross thread check
  ClassicScript::CreateUnspecifiedScript(bar0)->RunScriptOnScriptState(
      global_scopes[0]->ScriptController()->GetScriptState());

  EXPECT_TRUE(global_scopes[0]->FindDefinition("bar"));
  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("bar"));

  ClassicScript::CreateUnspecifiedScript(bar0)->RunScriptOnScriptState(
      global_scopes[1]->ScriptController()->GetScriptState());

  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("bar"));

  definition = global_scopes[0]->FindDefinition("bar");

  // Manually change the custom properties
  Vector<AtomicString> bar_custom_properties = {AtomicString("--bar1")};

  paint_worklet_to_test->RegisterMainThreadDocumentPaintDefinition(
      "bar", definition->NativeInvalidationProperties(), bar_custom_properties,
      definition->InputArgumentTypes(),
      definition->GetPaintRenderingContext2DSettings()->alpha());

  // Although the main thread definitions were the same, the definition sent
  // cross thread differed from the main thread definitions so it must become
  // invalid.
  EXPECT_FALSE(paint_worklet_to_test->GetDocumentDefinitionMap().at("bar"));

  // Definition invalidated by second main thread call after cross thread check
  ClassicScript::CreateUnspecifiedScript(loo0)->RunScriptOnScriptState(
      global_scopes[0]->ScriptController()->GetScriptState());

  EXPECT_TRUE(global_scopes[0]->FindDefinition("loo"));
  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("loo"));

  definition = global_scopes[0]->FindDefinition("loo");

  paint_worklet_to_test->RegisterMainThreadDocumentPaintDefinition(
      "loo", definition->NativeInvalidationProperties(),
      definition->CustomInvalidationProperties(),
      definition->InputArgumentTypes(),
      definition->GetPaintRenderingContext2DSettings()->alpha());

  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("loo"));

  ClassicScript::CreateUnspecifiedScript(loo1)->RunScriptOnScriptState(
      global_scopes[1]->ScriptController()->GetScriptState());

  // Although the first main thread call and the cross thread definition are the
  // same, the second main thread call differs so the definition must become
  // invalid
  EXPECT_FALSE(paint_worklet_to_test->GetDocumentDefinitionMap().at("loo"));

  // Definition invalidated by cross thread check before second main thread call
  ClassicScript::CreateUnspecifiedScript(gar0)->RunScriptOnScriptState(
      global_scopes[0]->ScriptController()->GetScriptState());

  EXPECT_TRUE(global_scopes[0]->FindDefinition("gar"));
  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("gar"));

  definition = global_scopes[0]->FindDefinition("gar");

  // Manually change custom properties
  Vector<AtomicString> gar_custom_properties = {AtomicString("--gar1")};

  paint_worklet_to_test->RegisterMainThreadDocumentPaintDefinition(
      "gar", definition->NativeInvalidationProperties(), gar_custom_properties,
      definition->InputArgumentTypes(),
      definition->GetPaintRenderingContext2DSettings()->alpha());

  EXPECT_FALSE(paint_worklet_to_test->GetDocumentDefinitionMap().at("gar"));

  ClassicScript::CreateUnspecifiedScript(gar0)->RunScriptOnScriptState(
      global_scopes[1]->ScriptController()->GetScriptState());

  // Although the main thread definitions were the same, the definition sent
  // cross thread differed from the main thread definitions so it must stay
  // invalid.
  EXPECT_FALSE(paint_worklet_to_test->GetDocumentDefinitionMap().at("gar"));
}

TEST_F(PaintWorkletTest, GeneratorNotifiedAfterAllRegistrations) {
  PaintWorklet* paint_worklet_to_test =
      PaintWorklet::From(*GetFrame().GetDocument()->domWindow());
  paint_worklet_to_test->ResetIsPaintOffThreadForTesting(true);

  MockObserver* observer = MakeGarbageCollected<MockObserver>();
  CSSPaintImageGeneratorImpl* generator =
      MakeGarbageCollected<CSSPaintImageGeneratorImpl>(
          observer, paint_worklet_to_test, "foo");
  paint_worklet_to_test->AddPendingGenerator("foo", generator);

  // The generator should not fire until the cross thread check
  EXPECT_CALL(*observer, PaintImageGeneratorReady).Times(0);

  Vector<Persistent<PaintWorkletGlobalScope>> global_scopes;
  for (wtf_size_t i = 0; i < PaintWorklet::kNumGlobalScopesPerThread; ++i) {
    paint_worklet_to_test->AddGlobalScopeForTesting();
    global_scopes.push_back(
        PaintWorkletGlobalScopeProxy::From(
            paint_worklet_to_test->GetGlobalScopesForTesting()[i])
            ->global_scope());
  }

  String foo = R"JS(registerPaint('foo', class {
        static get inputProperties() { return ['--foo']; }
        paint() {}
      });)JS";

  ClassicScript::CreateUnspecifiedScript(foo)->RunScriptOnScriptState(
      global_scopes[0]->ScriptController()->GetScriptState());

  EXPECT_TRUE(global_scopes[0]->FindDefinition("foo"));
  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("foo"));

  ClassicScript::CreateUnspecifiedScript(foo)->RunScriptOnScriptState(
      global_scopes[1]->ScriptController()->GetScriptState());

  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("foo"));
  EXPECT_FALSE(generator->IsImageGeneratorReady());

  CSSPaintDefinition* definition = global_scopes[0]->FindDefinition("foo");

  // The cross thread check should cause the generator to fire
  EXPECT_CALL(*observer, PaintImageGeneratorReady).Times(1);

  paint_worklet_to_test->RegisterMainThreadDocumentPaintDefinition(
      "foo", definition->NativeInvalidationProperties(),
      definition->CustomInvalidationProperties(),
      definition->InputArgumentTypes(),
      definition->GetPaintRenderingContext2DSettings()->alpha());

  EXPECT_TRUE(paint_worklet_to_test->GetDocumentDefinitionMap().at("foo"));
  EXPECT_TRUE(generator->IsImageGeneratorReady());
}

}  // namespace blink
