/* * Copyright (C) 2021 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #import "TestCocoa.h" #import "TestWKWebView.h" #import "Utilities.h" #import #import #import #import #import #import #import #import #define EXPECT_IN_RANGE(actual, min, max) \ EXPECT_GE(actual, min); \ EXPECT_LE(actual, max); @interface TestKVOWrapper : NSObject - (instancetype)initWithObservable:(NSObject *)observable keyPath:(NSString *)keyPath callback:(Function&&)callback; @end @implementation TestKVOWrapper { RetainPtr _observable; RetainPtr _keyPath; Function _callback; } - (instancetype)initWithObservable:(NSObject *)observable keyPath:(NSString *)keyPath callback:(Function&&)callback { if (!(self = [super init])) return nil; _observable = observable; _keyPath = keyPath; _callback = WTFMove(callback); [_observable addObserver:self forKeyPath:_keyPath.get() options:0 context:nil]; return self; } - (void)dealloc { [_observable removeObserver:self forKeyPath:_keyPath.get() context:nullptr]; [super dealloc]; } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { _callback(); } @end static RetainPtr createWebViewWithSampledPageTopColorMaxDifference(double sampledPageTopColorMaxDifference, double sampledPageTopColorMinHeight = 0) { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); [configuration _setSampledPageTopColorMaxDifference:sampledPageTopColorMaxDifference]; [configuration _setSampledPageTopColorMinHeight:sampledPageTopColorMinHeight]; return adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]); } static void waitForSampledPageTopColorToChange(TestWKWebView *webView, Function&& trigger = nullptr) { bool done = false; auto sampledPageTopColorObserver = adoptNS([[TestKVOWrapper alloc] initWithObservable:webView keyPath:@"_sampledPageTopColor" callback:[&] { done = true; }]); if (trigger) trigger(); TestWebKitAPI::Util::run(&done); } static void waitForSampledPageTopColorToChangeForHTML(TestWKWebView *webView, String&& html) { waitForSampledPageTopColorToChange(webView, [webView, html = WTFMove(html)] () mutable { [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:WTFMove(html)]; }); } static String createHTMLGradientWithColorStops(String&& direction, Vector&& colorStops) { EXPECT_GE(colorStops.size(), 2UL); StringBuilder gradientBuilder; gradientBuilder.append("Test"_s); return gradientBuilder.toString(); } TEST(SampledPageTopColor, ZeroMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(0); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:createHTMLGradientWithColorStops("right"_s, { "red"_s, "red"_s })]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, NegativeMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(-5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:createHTMLGradientWithColorStops("right"_s, { "red"_s, "red"_s })]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, SolidColor) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); waitForSampledPageTopColorToChangeForHTML(webView.get(), createHTMLGradientWithColorStops("right"_s, { "red"_s, "red"_s })); EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::red); } TEST(SampledPageTopColor, DifferentColorsWithoutOutlierBelowMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); waitForSampledPageTopColorToChangeForHTML(webView.get(), createHTMLGradientWithColorStops("right"_s, { "lab(1% 0 0)"_s, "lab(2% 0 0)"_s, "lab(3% 0 0)"_s, "lab(4% 0 0)"_s, "lab(5% 0 0)"_s, })); auto components = CGColorGetComponents([webView _sampledPageTopColor].CGColor); EXPECT_IN_RANGE(components[0], 0.04, 0.05); EXPECT_IN_RANGE(components[1], 0.04, 0.05); EXPECT_IN_RANGE(components[2], 0.04, 0.05); EXPECT_EQ(components[3], 1); } TEST(SampledPageTopColor, DifferentColorsWithLeftOutlierAboveMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); waitForSampledPageTopColorToChangeForHTML(webView.get(), createHTMLGradientWithColorStops("right"_s, { "lab(100% 0 0)"_s, // outlier "lab(1% 0 0)"_s, "lab(2% 0 0)"_s, "lab(3% 0 0)"_s, "lab(4% 0 0)"_s, })); auto components = CGColorGetComponents([webView _sampledPageTopColor].CGColor); EXPECT_IN_RANGE(components[0], 0.03, 0.04); EXPECT_IN_RANGE(components[1], 0.03, 0.04); EXPECT_IN_RANGE(components[2], 0.03, 0.04); EXPECT_EQ(components[3], 1); } TEST(SampledPageTopColor, DifferentColorsWithMiddleOutlierAboveMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:createHTMLGradientWithColorStops("right"_s, { "lab(1% 0 0)"_s, "lab(2% 0 0)"_s, "lab(100% 0 0)"_s, // outlier "lab(3% 0 0)"_s, "lab(4% 0 0)"_s, })]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, DifferentColorsWithRightOutlierAboveMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); waitForSampledPageTopColorToChangeForHTML(webView.get(), createHTMLGradientWithColorStops("right"_s, { "lab(1% 0 0)"_s, "lab(2% 0 0)"_s, "lab(3% 0 0)"_s, "lab(4% 0 0)"_s, "lab(100% 0 0)"_s, // outlier })); auto components = CGColorGetComponents([webView _sampledPageTopColor].CGColor); EXPECT_IN_RANGE(components[0], 0.03, 0.04); EXPECT_IN_RANGE(components[1], 0.03, 0.04); EXPECT_IN_RANGE(components[2], 0.03, 0.04); EXPECT_EQ(components[3], 1); } TEST(SampledPageTopColor, DifferentColorsIndividuallyAboveMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:createHTMLGradientWithColorStops("right"_s, { "lab(10% 0 0)"_s, "lab(20% 0 0)"_s, "lab(30% 0 0)"_s, "lab(40% 0 0)"_s, "lab(50% 0 0)"_s, })]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, DifferentColorsCumulativelyAboveMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:createHTMLGradientWithColorStops("right"_s, { "lab(1% 0 0)"_s, "lab(3% 0 0)"_s, "lab(5% 0 0)"_s, "lab(7% 0 0)"_s, "lab(9% 0 0)"_s, })]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, VerticalGradientBelowMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5, 100); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:createHTMLGradientWithColorStops("bottom"_s, { "lab(1% 0 0)"_s, "lab(2% 0 0)"_s, "lab(3% 0 0)"_s, "lab(4% 0 0)"_s, "lab(5% 0 0)"_s, "lab(6% 0 0)"_s, })]; auto components = CGColorGetComponents([webView _sampledPageTopColor].CGColor); EXPECT_IN_RANGE(components[0], 0.01, 0.02); EXPECT_IN_RANGE(components[1], 0.01, 0.02); EXPECT_IN_RANGE(components[2], 0.01, 0.02); EXPECT_EQ(components[3], 1); } TEST(SampledPageTopColor, VerticalGradientAboveMaxDifference) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5, 100); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:createHTMLGradientWithColorStops("bottom"_s, { "red"_s, "blue"_s })]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, HitTestHTMLImage) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"Test"]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, HitTestHTMLCanvasWithoutRenderingContext) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); waitForSampledPageTopColorToChangeForHTML(webView.get(), @"Test"); EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::red); } TEST(SampledPageTopColor, HitTestHTMLCanvasWithRenderingContext) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"Test"]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, HitTestCSSBackgroundImage) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"
Test"]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, HitTestBeforeCSSTransition) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); waitForSampledPageTopColorToChangeForHTML(webView.get(), @"Test"); EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white); } TEST(SampledPageTopColor, HitTestDuringCSSTransition) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@""]; [webView objectByEvaluatingJavaScript:@"document.body.style.setProperty('background-color', 'red')"]; TestWebKitAPI::Util::sleep(1); // Not setting this until now prevents the sampling logic from running because without it the page isn't considered contentful. [webView objectByEvaluatingJavaScript:@"document.body.textContent = 'Test'"]; [webView waitForNextPresentationUpdate]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, HitTestAfterCSSTransition) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@""]; [webView objectByEvaluatingJavaScript:@"document.body.style.setProperty('background-color', 'red')"]; TestWebKitAPI::Util::sleep(1); // Not setting this until now prevents the sampling logic from running because without it the page isn't considered contentful. [webView objectByEvaluatingJavaScript:@"document.body.textContent = 'Test'"]; waitForSampledPageTopColorToChange(webView.get()); EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::red); } TEST(SampledPageTopColor, HitTestBeforeCSSAnimation) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); waitForSampledPageTopColorToChangeForHTML(webView.get(), @"Test"); EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white); } TEST(SampledPageTopColor, HitTestDuringCSSAnimation) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@""]; TestWebKitAPI::Util::sleep(1); // Not setting this until now prevents the sampling logic from running because without it the page isn't considered contentful. [webView objectByEvaluatingJavaScript:@"document.body.textContent = 'Test'"]; [webView waitForNextPresentationUpdate]; EXPECT_NULL([webView _sampledPageTopColor]); } TEST(SampledPageTopColor, HitTestAfterCSSAnimation) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@""]; TestWebKitAPI::Util::sleep(1); // Not setting this until now prevents the sampling logic from running because without it the page isn't considered contentful. [webView objectByEvaluatingJavaScript:@"document.body.textContent = 'Test'"]; waitForSampledPageTopColorToChange(webView.get()); EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::red); } TEST(SampledPageTopColor, HitTestCSSPointerEventsNone) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"
Test"]; EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::red); } // FIXME: (Sampled Page Top Color: hook into painting logic instead of taking snapshots) TEST(SampledPageTopColor, DISABLED_DisplayP3) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:createHTMLGradientWithColorStops("right"_s, { "color(display-p3 0.99 0 0)"_s, "color(display-p3 0.98 0 0)"_s, "color(display-p3 0.97 0 0)"_s, "color(display-p3 0.96 0 0)"_s, "color(display-p3 0.95 0 0)"_s, })]; auto components = CGColorGetComponents([webView _sampledPageTopColor].CGColor); EXPECT_IN_RANGE(components[0], 0.3, 0.4); EXPECT_IN_RANGE(components[1], 1.00, 1.01); EXPECT_IN_RANGE(components[2], 1.00, 1.01); EXPECT_EQ(components[3], 1); } TEST(SampledPageTopColor, MainDocumentChange) { auto webView = createWebViewWithSampledPageTopColorMaxDifference(5); EXPECT_NULL([webView _sampledPageTopColor]); size_t notificationCount = 0; auto sampledPageTopColorObserver = adoptNS([[TestKVOWrapper alloc] initWithObservable:webView.get() keyPath:@"_sampledPageTopColor" callback:[&] { ++notificationCount; }]); [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"Test"]; EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white); EXPECT_EQ(notificationCount, 1UL); notificationCount = 0; [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"Test"]; EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white); // Depending on timing, a notification can be sent for when the main document changes and then // when the new main document renders or both can be coalesced if rendering is fast enough. EXPECT_TRUE(notificationCount == 0 || notificationCount == 2); notificationCount = 0; [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@""]; EXPECT_NULL([webView _sampledPageTopColor]); EXPECT_EQ(notificationCount, 1UL); notificationCount = 0; [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@""]; EXPECT_NULL([webView _sampledPageTopColor]); EXPECT_EQ(notificationCount, 0UL); notificationCount = 0; [webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"Test"]; EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white); EXPECT_EQ(notificationCount, 1UL); }