blob: 46ec4befb3b208777f6617ff5e649ebeed1271a6 [file] [log] [blame]
/*
* 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 <WebCore/Color.h>
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKWebViewConfigurationPrivate.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/_WKInternalDebugFeature.h>
#import <wtf/Function.h>
#import <wtf/RetainPtr.h>
#import <wtf/text/StringBuilder.h>
#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<void()>&&)callback;
@end
@implementation TestKVOWrapper {
RetainPtr<NSObject> _observable;
RetainPtr<NSString> _keyPath;
Function<void()> _callback;
}
- (instancetype)initWithObservable:(NSObject *)observable keyPath:(NSString *)keyPath callback:(Function<void()>&&)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<TestWKWebView> 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<void()>&& 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<String>&& colorStops)
{
EXPECT_GE(colorStops.size(), 2UL);
StringBuilder gradientBuilder;
gradientBuilder.append("<body style=\"background-image: linear-gradient(to "_s);
gradientBuilder.append(WTFMove(direction));
for (auto&& colorStop : WTFMove(colorStops)) {
gradientBuilder.append(", "_s);
gradientBuilder.append(WTFMove(colorStop));
}
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:@"<body style='margin: 0'><img src='enormous.svg' style='width: 100%; height: 100%'>Test"];
EXPECT_NULL([webView _sampledPageTopColor]);
}
TEST(SampledPageTopColor, HitTestHTMLCanvasWithoutRenderingContext)
{
auto webView = createWebViewWithSampledPageTopColorMaxDifference(5);
EXPECT_NULL([webView _sampledPageTopColor]);
waitForSampledPageTopColorToChangeForHTML(webView.get(), @"<body style='margin: 0'><canvas style='width: 100%; height: 100%; background-color: red'></canvas>Test");
EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::red);
}
TEST(SampledPageTopColor, HitTestHTMLCanvasWithRenderingContext)
{
auto webView = createWebViewWithSampledPageTopColorMaxDifference(5);
EXPECT_NULL([webView _sampledPageTopColor]);
[webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body style='margin: 0'><canvas style='width: 100%; height: 100%; background-color: red'></canvas>Test<script>document.querySelector('canvas').getContext('2d')</script>"];
EXPECT_NULL([webView _sampledPageTopColor]);
}
TEST(SampledPageTopColor, HitTestCSSBackgroundImage)
{
auto webView = createWebViewWithSampledPageTopColorMaxDifference(5);
EXPECT_NULL([webView _sampledPageTopColor]);
[webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body style='margin: 0;'><div style='width: 100%; height: 100%; background-image: url(\'enormous.svg\')'>Test"];
EXPECT_NULL([webView _sampledPageTopColor]);
}
TEST(SampledPageTopColor, HitTestBeforeCSSTransition)
{
auto webView = createWebViewWithSampledPageTopColorMaxDifference(5);
EXPECT_NULL([webView _sampledPageTopColor]);
waitForSampledPageTopColorToChangeForHTML(webView.get(), @"<body style='margin: 0; transition: background-color 1s'>Test");
EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white);
}
TEST(SampledPageTopColor, HitTestDuringCSSTransition)
{
auto webView = createWebViewWithSampledPageTopColorMaxDifference(5);
EXPECT_NULL([webView _sampledPageTopColor]);
[webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body style='margin: 0; transition: background-color 1000s'>"];
[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:@"<body style='margin: 0; transition: background-color 0.1s'>"];
[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(), @"<style>@keyframes changeBackgroundRed { to { background-color: red; } }</style><body style='margin: 0; animation: changeBackgroundRed 1s forwards paused'>Test");
EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white);
}
TEST(SampledPageTopColor, HitTestDuringCSSAnimation)
{
auto webView = createWebViewWithSampledPageTopColorMaxDifference(5);
EXPECT_NULL([webView _sampledPageTopColor]);
[webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<style>@keyframes changeBackgroundRed { to { background-color: red; } }</style><body style='margin: 0; animation: changeBackgroundRed 1000s forwards'>"];
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:@"<style>@keyframes changeBackgroundRed { to { background-color: red; } }</style><body style='margin: 0; animation: changeBackgroundRed 0.1s forwards'>"];
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:@"<body style='margin: 0'><div style='width: 100%; height: 100%; background-color: red; pointer-events: none'></div>Test"];
EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::red);
}
// FIXME: <https://webkit.org/b/225167> (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:@"<body>Test"];
EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white);
EXPECT_EQ(notificationCount, 1UL);
notificationCount = 0;
[webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body>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:@"<body>"];
EXPECT_NULL([webView _sampledPageTopColor]);
EXPECT_EQ(notificationCount, 1UL);
notificationCount = 0;
[webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body>"];
EXPECT_NULL([webView _sampledPageTopColor]);
EXPECT_EQ(notificationCount, 0UL);
notificationCount = 0;
[webView synchronouslyLoadHTMLStringAndWaitUntilAllImmediateChildFramesPaint:@"<body>Test"];
EXPECT_EQ(WebCore::roundAndClampToSRGBALossy([webView _sampledPageTopColor].CGColor), WebCore::Color::white);
EXPECT_EQ(notificationCount, 1UL);
}