| // Copyright 2018 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/core/css/css_paint_value.h" |
| |
| #include <memory> |
| #include "base/auto_reset.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| #include "third_party/blink/renderer/core/css/css_custom_ident_value.h" |
| #include "third_party/blink/renderer/core/css/css_syntax_definition.h" |
| #include "third_party/blink/renderer/core/css/mock_css_paint_image_generator.h" |
| #include "third_party/blink/renderer/core/dom/document.h" |
| #include "third_party/blink/renderer/core/frame/frame_test_helpers.h" |
| #include "third_party/blink/renderer/core/frame/web_local_frame_impl.h" |
| #include "third_party/blink/renderer/core/layout/layout_object.h" |
| #include "third_party/blink/renderer/core/style/computed_style.h" |
| #include "third_party/blink/renderer/core/style/style_generated_image.h" |
| #include "third_party/blink/renderer/core/testing/core_unit_test_helper.h" |
| #include "third_party/blink/renderer/platform/graphics/paint_generated_image.h" |
| #include "third_party/blink/renderer/platform/testing/histogram_tester.h" |
| #include "third_party/blink/renderer/platform/testing/unit_test_helpers.h" |
| #include "third_party/blink/renderer/platform/testing/url_test_helpers.h" |
| |
| using testing::_; |
| using testing::NiceMock; |
| using testing::Return; |
| using testing::ReturnRef; |
| using testing::Values; |
| |
| namespace blink { |
| namespace { |
| |
| enum { |
| kCSSPaintAPIArguments = 1 << 0, |
| kOffMainThreadCSSPaint = 1 << 1, |
| }; |
| |
| class CSSPaintValueTest : public RenderingTest, |
| public ::testing::WithParamInterface<unsigned>, |
| private ScopedCSSPaintAPIArgumentsForTest, |
| private ScopedOffMainThreadCSSPaintForTest { |
| public: |
| CSSPaintValueTest() |
| : ScopedCSSPaintAPIArgumentsForTest(GetParam() & kCSSPaintAPIArguments), |
| ScopedOffMainThreadCSSPaintForTest(GetParam() & |
| kOffMainThreadCSSPaint) {} |
| |
| // TODO(xidachen): a mock_generator is used in many tests in this file, put |
| // that in a Setup method. |
| }; |
| |
| INSTANTIATE_TEST_SUITE_P(All, |
| CSSPaintValueTest, |
| Values(0, |
| kCSSPaintAPIArguments, |
| kOffMainThreadCSSPaint, |
| kCSSPaintAPIArguments | |
| kOffMainThreadCSSPaint)); |
| |
| // CSSPaintImageGenerator requires that CSSPaintImageGeneratorCreateFunction be |
| // a static method. As such, it cannot access a class member and so instead we |
| // store a pointer to the overriding generator globally. |
| MockCSSPaintImageGenerator* g_override_generator = nullptr; |
| CSSPaintImageGenerator* ProvideOverrideGenerator( |
| const String&, |
| const Document&, |
| CSSPaintImageGenerator::Observer*) { |
| return g_override_generator; |
| } |
| } // namespace |
| |
| TEST_P(CSSPaintValueTest, ReportingCompositedUMA) { |
| HistogramTester histogram_tester; |
| NiceMock<MockCSSPaintImageGenerator>* mock_generator = |
| MakeGarbageCollected<NiceMock<MockCSSPaintImageGenerator>>(); |
| base::AutoReset<MockCSSPaintImageGenerator*> scoped_override_generator( |
| &g_override_generator, mock_generator); |
| base::AutoReset<CSSPaintImageGenerator::CSSPaintImageGeneratorCreateFunction> |
| scoped_create_function( |
| CSSPaintImageGenerator::GetCreateFunctionForTesting(), |
| ProvideOverrideGenerator); |
| |
| const FloatSize target_size(100, 100); |
| |
| SetBodyInnerHTML(R"HTML(<div id="target"></div>)HTML"); |
| LayoutObject* target = GetLayoutObjectByElementId("target"); |
| const ComputedStyle& style = *target->Style(); |
| |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("testpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| // Mark the generator as ready - GetImage should succeed when |
| // OffMainThreadCSSPaint is enabled. |
| ON_CALL(*mock_generator, IsImageGeneratorReady()).WillByDefault(Return(true)); |
| ON_CALL(*mock_generator, Paint(_, _, _, _)) |
| .WillByDefault(Return(PaintGeneratedImage::Create(nullptr, target_size))); |
| ASSERT_TRUE( |
| paint_value->GetImage(*target, GetDocument(), style, target_size)); |
| if (RuntimeEnabledFeatures::OffMainThreadCSSPaintEnabled()) { |
| histogram_tester.ExpectTotalCount("Blink.CSSPaintValue.PaintOffThread", 1u); |
| histogram_tester.ExpectUniqueSample("Blink.CSSPaintValue.PaintOffThread", |
| true, 1u); |
| } else { |
| histogram_tester.ExpectTotalCount("Blink.CSSPaintValue.PaintOffThread", 0u); |
| } |
| |
| ASSERT_TRUE( |
| paint_value->GetImage(*target, GetDocument(), style, target_size)); |
| if (RuntimeEnabledFeatures::OffMainThreadCSSPaintEnabled()) { |
| // Repaint, should not report histogram again. |
| histogram_tester.ExpectTotalCount("Blink.CSSPaintValue.PaintOffThread", 1u); |
| histogram_tester.ExpectUniqueSample("Blink.CSSPaintValue.PaintOffThread", |
| true, 1u); |
| } else { |
| histogram_tester.ExpectTotalCount("Blink.CSSPaintValue.PaintOffThread", 0u); |
| } |
| } |
| |
| TEST_P(CSSPaintValueTest, ReportingNonCompositedUMA) { |
| HistogramTester histogram_tester; |
| NiceMock<MockCSSPaintImageGenerator>* mock_generator = |
| MakeGarbageCollected<NiceMock<MockCSSPaintImageGenerator>>(); |
| mock_generator->AddNativeProperty(); |
| base::AutoReset<MockCSSPaintImageGenerator*> scoped_override_generator( |
| &g_override_generator, mock_generator); |
| base::AutoReset<CSSPaintImageGenerator::CSSPaintImageGeneratorCreateFunction> |
| scoped_create_function( |
| CSSPaintImageGenerator::GetCreateFunctionForTesting(), |
| ProvideOverrideGenerator); |
| |
| const FloatSize target_size(100, 100); |
| |
| SetBodyInnerHTML(R"HTML(<div id="target"></div>)HTML"); |
| LayoutObject* target = GetLayoutObjectByElementId("target"); |
| auto style = ComputedStyle::Create(); |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("testpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| StyleGeneratedImage* style_image = |
| MakeGarbageCollected<StyleGeneratedImage>(*paint_value); |
| style->SetBorderImageSource(style_image); |
| |
| ON_CALL(*mock_generator, IsImageGeneratorReady()).WillByDefault(Return(true)); |
| EXPECT_CALL(*mock_generator, Paint(_, _, _, _)) |
| .WillRepeatedly( |
| Return(PaintGeneratedImage::Create(nullptr, target_size))); |
| // The paint worklet is not composited, and falls back to the main thread |
| // paint. |
| ASSERT_TRUE( |
| paint_value->GetImage(*target, GetDocument(), *style, target_size)); |
| if (RuntimeEnabledFeatures::OffMainThreadCSSPaintEnabled()) { |
| histogram_tester.ExpectTotalCount("Blink.CSSPaintValue.PaintOffThread", 1u); |
| histogram_tester.ExpectUniqueSample("Blink.CSSPaintValue.PaintOffThread", |
| false, 1u); |
| } else { |
| histogram_tester.ExpectTotalCount("Blink.CSSPaintValue.PaintOffThread", 0u); |
| } |
| |
| // Repaint, should not report histogram again. |
| ASSERT_TRUE( |
| paint_value->GetImage(*target, GetDocument(), *style, target_size)); |
| if (RuntimeEnabledFeatures::OffMainThreadCSSPaintEnabled()) { |
| histogram_tester.ExpectTotalCount("Blink.CSSPaintValue.PaintOffThread", 1u); |
| histogram_tester.ExpectUniqueSample("Blink.CSSPaintValue.PaintOffThread", |
| false, 1u); |
| } else { |
| histogram_tester.ExpectTotalCount("Blink.CSSPaintValue.PaintOffThread", 0u); |
| } |
| } |
| |
| TEST_P(CSSPaintValueTest, DelayPaintUntilGeneratorReady) { |
| NiceMock<MockCSSPaintImageGenerator>* mock_generator = |
| MakeGarbageCollected<NiceMock<MockCSSPaintImageGenerator>>(); |
| base::AutoReset<MockCSSPaintImageGenerator*> scoped_override_generator( |
| &g_override_generator, mock_generator); |
| base::AutoReset<CSSPaintImageGenerator::CSSPaintImageGeneratorCreateFunction> |
| scoped_create_function( |
| CSSPaintImageGenerator::GetCreateFunctionForTesting(), |
| ProvideOverrideGenerator); |
| |
| const FloatSize target_size(100, 100); |
| |
| SetBodyInnerHTML(R"HTML( |
| <div id="target"></div> |
| )HTML"); |
| LayoutObject* target = GetLayoutObjectByElementId("target"); |
| const ComputedStyle& style = *target->Style(); |
| |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("testpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| |
| // Initially the generator is not ready, so GetImage should fail (and no paint |
| // should happen). |
| EXPECT_CALL(*mock_generator, Paint(_, _, _, _)).Times(0); |
| EXPECT_FALSE( |
| paint_value->GetImage(*target, GetDocument(), style, target_size)); |
| |
| // Now mark the generator as ready - GetImage should then succeed. |
| ON_CALL(*mock_generator, IsImageGeneratorReady()).WillByDefault(Return(true)); |
| // In off-thread CSS Paint, the actual paint call is deferred and so will |
| // never happen. |
| if (!RuntimeEnabledFeatures::OffMainThreadCSSPaintEnabled()) { |
| EXPECT_CALL(*mock_generator, Paint(_, _, _, _)) |
| .WillRepeatedly( |
| Return(PaintGeneratedImage::Create(nullptr, target_size))); |
| } |
| |
| EXPECT_TRUE( |
| paint_value->GetImage(*target, GetDocument(), style, target_size)); |
| } |
| |
| // Regression test for crbug.com/998439. The problem is that GetImage is called |
| // on a new document. This test simulates the situation by having two different |
| // documents and call GetImage on different ones. |
| TEST_P(CSSPaintValueTest, GetImageCalledOnMultipleDocuments) { |
| const FloatSize target_size(100, 100); |
| |
| SetBodyInnerHTML(R"HTML(<div id="target"></div>)HTML"); |
| LayoutObject* target = GetLayoutObjectByElementId("target"); |
| const ComputedStyle& style = *target->Style(); |
| |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("testpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| |
| EXPECT_EQ(paint_value->NumberOfGeneratorsForTesting(), 0u); |
| paint_value->GetImage(*target, GetDocument(), style, target_size); |
| // A new generator should be created if there is no generator exists. |
| EXPECT_EQ(paint_value->NumberOfGeneratorsForTesting(), 1u); |
| |
| auto new_page_holder = std::make_unique<DummyPageHolder>(IntSize(800, 600)); |
| // Call GetImage on a new Document should not crash. |
| paint_value->GetImage(*target, new_page_holder->GetDocument(), style, |
| target_size); |
| EXPECT_EQ(paint_value->NumberOfGeneratorsForTesting(), 2u); |
| } |
| |
| TEST_P(CSSPaintValueTest, NativeInvalidationPropertiesWithNoGenerator) { |
| SetBodyInnerHTML(R"HTML(<div id="target"></div>)HTML"); |
| |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("testpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| |
| EXPECT_EQ(paint_value->NumberOfGeneratorsForTesting(), 0u); |
| // There is no generator, so returning a nullptr. |
| EXPECT_EQ(paint_value->NativeInvalidationProperties(GetDocument()), nullptr); |
| } |
| |
| TEST_P(CSSPaintValueTest, CustomInvalidationPropertiesWithNoGenerator) { |
| SetBodyInnerHTML(R"HTML(<div id="target"></div>)HTML"); |
| |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("testpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| |
| EXPECT_EQ(paint_value->NumberOfGeneratorsForTesting(), 0u); |
| // There is no generator, so returning a nullptr. |
| EXPECT_EQ(paint_value->CustomInvalidationProperties(GetDocument()), nullptr); |
| } |
| |
| TEST_P(CSSPaintValueTest, PrintingMustFallbackToMainThread) { |
| if (!RuntimeEnabledFeatures::OffMainThreadCSSPaintEnabled()) |
| return; |
| |
| NiceMock<MockCSSPaintImageGenerator>* mock_generator = |
| MakeGarbageCollected<NiceMock<MockCSSPaintImageGenerator>>(); |
| base::AutoReset<MockCSSPaintImageGenerator*> scoped_override_generator( |
| &g_override_generator, mock_generator); |
| base::AutoReset<CSSPaintImageGenerator::CSSPaintImageGeneratorCreateFunction> |
| scoped_create_function( |
| CSSPaintImageGenerator::GetCreateFunctionForTesting(), |
| ProvideOverrideGenerator); |
| |
| const FloatSize target_size(100, 100); |
| |
| SetBodyInnerHTML(R"HTML( |
| <div id="target"></div> |
| )HTML"); |
| LayoutObject* target = GetLayoutObjectByElementId("target"); |
| const ComputedStyle& style = *target->Style(); |
| |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("testpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| |
| ON_CALL(*mock_generator, IsImageGeneratorReady()).WillByDefault(Return(true)); |
| // This PW can be composited, so we should only fall back to main once, in |
| // the case where we are printing. |
| EXPECT_CALL(*mock_generator, Paint(_, _, _, _)) |
| .Times(1) |
| .WillOnce(Return(PaintGeneratedImage::Create(nullptr, target_size))); |
| |
| ASSERT_TRUE( |
| paint_value->GetImage(*target, GetDocument(), style, target_size)); |
| |
| // Start printing; our paint should run on the main thread (and thus call |
| // Paint). |
| GetDocument().SetPrinting(Document::kPrinting); |
| ASSERT_TRUE( |
| paint_value->GetImage(*target, GetDocument(), style, target_size)); |
| |
| // Stop printing; we should return to the compositor. |
| GetDocument().SetPrinting(Document::kNotPrinting); |
| ASSERT_TRUE( |
| paint_value->GetImage(*target, GetDocument(), style, target_size)); |
| } |
| |
| // Regression test for https://crbug.com/835589. |
| TEST_P(CSSPaintValueTest, DoNotPaintForLink) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| a { |
| background-image: paint(linkpainter); |
| width: 100px; |
| height: 100px; |
| } |
| </style> |
| <a href="http://www.example.com" id="target"></a> |
| )HTML"); |
| LayoutObject* target = GetLayoutObjectByElementId("target"); |
| const ComputedStyle& style = *target->Style(); |
| ASSERT_NE(style.InsideLink(), EInsideLink::kNotInsideLink); |
| |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("linkpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| EXPECT_FALSE(paint_value->GetImage(*target, GetDocument(), style, |
| FloatSize(100, 100))); |
| } |
| |
| // Regression test for https://crbug.com/835589. |
| TEST_P(CSSPaintValueTest, DoNotPaintWhenAncestorHasLink) { |
| SetBodyInnerHTML(R"HTML( |
| <style> |
| a { |
| width: 200px; |
| height: 200px; |
| } |
| b { |
| background-image: paint(linkpainter); |
| width: 100px; |
| height: 100px; |
| } |
| </style> |
| <a href="http://www.example.com" id="ancestor"> |
| <b id="target"></b> |
| </a> |
| )HTML"); |
| LayoutObject* target = GetLayoutObjectByElementId("target"); |
| const ComputedStyle& style = *target->Style(); |
| ASSERT_NE(style.InsideLink(), EInsideLink::kNotInsideLink); |
| |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("linkpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| EXPECT_FALSE(paint_value->GetImage(*target, GetDocument(), style, |
| FloatSize(100, 100))); |
| } |
| |
| TEST_P(CSSPaintValueTest, BuildInputArgumentValuesNotCrash) { |
| auto* ident = MakeGarbageCollected<CSSCustomIdentValue>("testpainter"); |
| CSSPaintValue* paint_value = MakeGarbageCollected<CSSPaintValue>(ident, true); |
| |
| ASSERT_EQ(paint_value->GetParsedInputArgumentsForTesting(), nullptr); |
| Vector<std::unique_ptr<CrossThreadStyleValue>> cross_thread_input_arguments; |
| paint_value->BuildInputArgumentValuesForTesting(cross_thread_input_arguments); |
| EXPECT_EQ(cross_thread_input_arguments.size(), 0u); |
| } |
| |
| } // namespace blink |