blob: ec33d0625907bc0b02e07dd6e0be0c3ae73fa663 [file] [log] [blame] [edit]
/*
* Copyright (C) 2024 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"
#if ENABLE(SCREEN_TIME)
#import "AppKitSPI.h"
#import "HTTPServer.h"
#import "InstanceMethodSwizzler.h"
#import "PlatformUtilities.h"
#import "Test.h"
#import "TestNavigationDelegate.h"
#import "TestWKWebView.h"
#import "Utilities.h"
#import <ScreenTime/STScreenTimeConfiguration.h>
#import <ScreenTime/STWebHistory.h>
#import <ScreenTime/STWebpageController.h>
#import <WebKit/WKPreferencesPrivate.h>
#import <WebKit/WKUIDelegatePrivate.h>
#import <WebKit/WKWebViewConfiguration.h>
#import <WebKit/WKWebViewPrivate.h>
#import <WebKit/WKWebViewPrivateForTesting.h>
#import <WebKit/WKWebsiteDataRecordPrivate.h>
#import <WebKit/WKWebsiteDataStore.h>
#import <WebKit/_WKFeature.h>
#import <pal/cocoa/ScreenTimeSoftLink.h>
#import <wtf/RetainPtr.h>
static void *blockedStateObserverChangeKVOContext = &blockedStateObserverChangeKVOContext;
static bool stateDidChange = false;
static bool receivedLoadMessage = false;
static bool hasVideoInPictureInPictureValue = false;
static bool hasVideoInPictureInPictureCalled = false;
static RetainPtr<TestWKWebView> webViewForScreenTimeTests(WKWebViewConfiguration *configuration = nil, BOOL addToWindow = YES)
{
if (!configuration)
configuration = adoptNS([[WKWebViewConfiguration alloc] init]).autorelease();
auto preferences = [configuration preferences];
for (_WKFeature *feature in [WKPreferences _features]) {
if ([feature.key isEqualToString:@"ScreenTimeEnabled"])
[preferences _setEnabled:YES forFeature:feature];
}
return adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 400, 300) configuration:configuration addToWindow:addToWindow]);
}
static std::unique_ptr<InstanceMethodSwizzler> swizzleEnforcesChildRestrictions(bool& done, bool enforces = YES)
{
return WTF::makeUnique<InstanceMethodSwizzler>(PAL::getSTScreenTimeConfigurationClassSingleton(), @selector(enforcesChildRestrictions), imp_implementationWithBlock(^BOOL {
done = true;
return enforces;
}));
}
static void testSuppressUsageRecordingWithDataStore(RetainPtr<WKWebsiteDataStore>&& websiteDataStore, bool suppressUsageRecordingExpectation)
{
__block bool done = false;
__block bool suppressUsageRecording = false;
__block bool childRestrictionsDone = false;
InstanceMethodSwizzler swizzler {
PAL::getSTWebpageControllerClassSingleton(),
@selector(setSuppressUsageRecording:),
imp_implementationWithBlock(^(id object, bool value) {
suppressUsageRecording = value;
done = true;
})
};
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration setWebsiteDataStore:websiteDataStore.get()];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
TestWebKitAPI::Util::run(&done);
EXPECT_EQ(suppressUsageRecordingExpectation, suppressUsageRecording);
}
@interface STWebpageController ()
@property (setter=setURLIsBlocked:) BOOL URLIsBlocked;
@end
@interface STWebHistory ()
@property (readonly, copy) STWebHistoryProfileIdentifier profileIdentifier;
@end
@interface WKWebView (Internal)
- (STWebpageController *)_screenTimeWebpageController;
#if PLATFORM(MAC)
- (NSVisualEffectView *) _screenTimeBlurredSnapshot;
#else
- (UIVisualEffectView *) _screenTimeBlurredSnapshot;
#endif
@end
@interface BlockedStateObserver : NSObject
- (instancetype)initWithWebView:(TestWKWebView *)webView;
@end
@implementation BlockedStateObserver {
RetainPtr<TestWKWebView> _webView;
}
- (instancetype)initWithWebView:(TestWKWebView *)webView
{
if (!(self = [super init]))
return nil;
_webView = webView;
[_webView addObserver:self forKeyPath:@"isBlockedByScreenTime" options:(NSKeyValueObservingOptionOld | NSKeyValueObservingOptionNew) context:&blockedStateObserverChangeKVOContext];
return self;
}
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if (context == &blockedStateObserverChangeKVOContext) {
stateDidChange = true;
return;
}
[super observeValueForKeyPath:keyPath ofObject:object change:change context:context];
}
@end
static BOOL blurredViewIsPresent(TestWKWebView *webView)
{
#if PLATFORM(IOS_FAMILY)
for (UIView *subview in [webView subviews]) {
if ([subview isKindOfClass:[UIVisualEffectView class]])
return true;
}
#else
for (NSView *subview in [webView subviews]) {
if ([subview isKindOfClass:[NSVisualEffectView class]])
return true;
}
#endif
return false;
}
static BOOL systemScreenTimeBlockingViewIsPresent(TestWKWebView *webView)
{
RetainPtr controller = [webView _screenTimeWebpageController];
#if PLATFORM(IOS_FAMILY)
for (UIView *subview in [webView subviews]) {
if (subview == [controller view])
return true;
}
#else
for (NSView *subview in [webView subviews]) {
if (subview == [controller view])
return true;
}
#endif
return false;
}
static RetainPtr<TestWKWebView> testShowsSystemScreenTimeBlockingView(bool showsSystemScreenTimeBlockingView)
{
__block bool childRestrictionsDone = false;
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration setShowsSystemScreenTimeBlockingView:showsSystemScreenTimeBlockingView];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
RetainPtr controller = [webView _screenTimeWebpageController];
[controller setURLIsBlocked:YES];
EXPECT_EQ(showsSystemScreenTimeBlockingView, [configuration showsSystemScreenTimeBlockingView]);
// Check if ScreenTime's blocking view is hidden or not.
EXPECT_EQ(showsSystemScreenTimeBlockingView, systemScreenTimeBlockingViewIsPresent(webView.get()));
// Check if WebKit's blurred blocking view is added and in the view hierarchy or not.
EXPECT_EQ(!showsSystemScreenTimeBlockingView, blurredViewIsPresent(webView.get()));
return webView;
}
#if PLATFORM(MAC)
static void testWebContentIsNotClickableShowingSystemScreenTimeBlockingView(bool showsSystemScreenTimeBlockingView)
{
__block bool childRestrictionsDone = false;
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration setShowsSystemScreenTimeBlockingView:showsSystemScreenTimeBlockingView];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr observer = adoptNS([[BlockedStateObserver alloc] initWithWebView:webView.get()]);
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadHTMLString:
@"<!DOCTYPE html>"
"<html>"
"<head>"
"<style>"
"body, html { margin: 0; width: 100%; height: 100%; }"
"</style>"
"</head>"
"<body>"
"<script>"
"let mouseDownCounter = 0;"
"addEventListener('mousedown', function() { mouseDownCounter += 1; });"
"</script>"
"</body>"
"</html>"
")" baseURL:[NSURL URLWithString:@"http://webkit.org"]];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
RetainPtr screenTimeController = [webView _screenTimeWebpageController];
[screenTimeController setURLIsBlocked:YES];
TestWebKitAPI::Util::run(&stateDidChange);
[webView sendClickAtPoint:NSMakePoint(300, 300)];
[webView waitForPendingMouseEvents];
stateDidChange = false;
[screenTimeController setURLIsBlocked:NO];
TestWebKitAPI::Util::run(&stateDidChange);
[webView sendClickAtPoint:NSMakePoint(300, 300)];
[webView waitForPendingMouseEvents];
int mouseDownCounter = [[webView objectByEvaluatingJavaScript:@"mouseDownCounter"] intValue];
EXPECT_EQ(mouseDownCounter, 1);
}
#endif
TEST(ScreenTime, IsBlockedByScreenTimeTrue)
{
__block bool childRestrictionsDone = false;
RetainPtr webView = webViewForScreenTimeTests();
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
RetainPtr controller = [webView _screenTimeWebpageController];
[controller setURLIsBlocked:YES];
EXPECT_TRUE([webView isBlockedByScreenTime]);
}
TEST(ScreenTime, IsBlockedByScreenTimeFalse)
{
__block bool childRestrictionsDone = false;
RetainPtr webView = webViewForScreenTimeTests();
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
RetainPtr controller = [webView _screenTimeWebpageController];
[controller setURLIsBlocked:NO];
EXPECT_FALSE([webView isBlockedByScreenTime]);
}
TEST(ScreenTime, IsBlockedByScreenTimeMultiple)
{
__block bool childRestrictionsDone = false;
RetainPtr webView = webViewForScreenTimeTests();
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
RetainPtr controller = [webView _screenTimeWebpageController];
[controller setURLIsBlocked:YES];
[controller setURLIsBlocked:NO];
[webView waitForNextPresentationUpdate];
EXPECT_FALSE([webView isBlockedByScreenTime]);
}
TEST(ScreenTime, IsBlockedByScreenTimeKVO)
{
__block bool childRestrictionsDone = false;
RetainPtr webView = webViewForScreenTimeTests();
auto observer = adoptNS([[BlockedStateObserver alloc] initWithWebView:webView.get()]);
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
RetainPtr controller = [webView _screenTimeWebpageController];
[controller setURLIsBlocked:YES];
TestWebKitAPI::Util::run(&stateDidChange);
EXPECT_TRUE([webView isBlockedByScreenTime]);
stateDidChange = false;
[controller setURLIsBlocked:NO];
TestWebKitAPI::Util::run(&stateDidChange);
EXPECT_FALSE([webView isBlockedByScreenTime]);
stateDidChange = false;
[controller setURLIsBlocked:YES];
TestWebKitAPI::Util::run(&stateDidChange);
EXPECT_TRUE([webView isBlockedByScreenTime]);
}
TEST(ScreenTime, IdentifierNil)
{
__block bool done = false;
__block NSString *identifier = @"testing123";
__block bool childRestrictionsDone = false;
InstanceMethodSwizzler swizzler {
PAL::getSTWebpageControllerClassSingleton(),
@selector(setProfileIdentifier:),
imp_implementationWithBlock(^(id object, NSString *profileIdentifier) {
identifier = profileIdentifier;
done = true;
})
};
RetainPtr webView = webViewForScreenTimeTests();
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
TestWebKitAPI::Util::run(&done);
EXPECT_NULL(identifier);
}
TEST(ScreenTime, IdentifierString)
{
__block bool done = false;
__block RetainPtr identifier = @"";
__block bool childRestrictionsDone = false;
InstanceMethodSwizzler swizzler {
PAL::getSTWebpageControllerClassSingleton(),
@selector(setProfileIdentifier:),
imp_implementationWithBlock(^(id object, NSString *profileIdentifier) {
identifier = profileIdentifier;
done = true;
})
};
RetainPtr uuid = [NSUUID UUID];
RetainPtr websiteDataStore = [WKWebsiteDataStore dataStoreForIdentifier:uuid.get()];
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration setWebsiteDataStore:websiteDataStore.get()];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
TestWebKitAPI::Util::run(&done);
EXPECT_WK_STREQ(identifier.get(), [uuid UUIDString]);
}
TEST(ScreenTime, IdentifierStringWithRemoveData)
{
__block bool done = false;
__block RetainPtr identifier = @"";
__block bool childRestrictionsDone = false;
RetainPtr dataTypeScreenTime = adoptNS([[NSSet alloc] initWithArray:@[ WKWebsiteDataTypeScreenTime ]]);
InstanceMethodSwizzler swizzler {
PAL::getSTWebHistoryClassSingleton(),
@selector(deleteHistoryDuringInterval:),
imp_implementationWithBlock(^(STWebHistory *object, NSDateInterval *) {
identifier = object.profileIdentifier;
})
};
RetainPtr uuid = [NSUUID UUID];
RetainPtr websiteDataStore = [WKWebsiteDataStore dataStoreForIdentifier:uuid.get()];
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration setWebsiteDataStore:websiteDataStore.get()];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://icloud.com"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[websiteDataStore removeDataOfTypes:dataTypeScreenTime.get() modifiedSince:[NSDate distantPast] completionHandler:^() {
done = true;
}];
TestWebKitAPI::Util::run(&done);
EXPECT_WK_STREQ(identifier.get(), [uuid UUIDString]);
}
TEST(ScreenTime, PersistentSession)
{
testSuppressUsageRecordingWithDataStore([WKWebsiteDataStore defaultDataStore], false);
}
TEST(ScreenTime, NonPersistentSession)
{
testSuppressUsageRecordingWithDataStore([WKWebsiteDataStore nonPersistentDataStore], true);
}
TEST(ScreenTime, ShowSystemScreenTimeBlockingTrue)
{
testShowsSystemScreenTimeBlockingView(true);
}
TEST(ScreenTime, ShowSystemScreenTimeBlockingFalse)
{
testShowsSystemScreenTimeBlockingView(false);
}
TEST(ScreenTime, ShowSystemScreenTimeBlockingFalseAndRemoved)
{
RetainPtr webView = testShowsSystemScreenTimeBlockingView(false);
RetainPtr controller = [webView _screenTimeWebpageController];
[controller setURLIsBlocked:NO];
EXPECT_FALSE([[webView configuration] showsSystemScreenTimeBlockingView]);
// Check if blurred blocking view is removed when URLIsBlocked is false.
EXPECT_FALSE(blurredViewIsPresent(webView.get()));
}
TEST(ScreenTime, WKWebViewFillsStackView)
{
CGRect windowRect = CGRectMake(0, 0, 800, 600);
RetainPtr webView = webViewForScreenTimeTests(nil);
[webView setTranslatesAutoresizingMaskIntoConstraints:NO];
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
#if PLATFORM(MAC)
RetainPtr stackView = adoptNS([[NSStackView alloc] init]);
RetainPtr window = adoptNS([[NSWindow alloc] initWithContentRect:NSRectFromCGRect(windowRect) styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]);
RetainPtr contentView = [window contentView];
#else
RetainPtr stackView = adoptNS([[UIStackView alloc] init]);
RetainPtr hostWindow = adoptNS([[UIWindow alloc] initWithFrame:windowRect]);
RetainPtr contentView = hostWindow;
#endif
[contentView addSubview:stackView.get()];
[stackView addArrangedSubview:webView.get()];
[stackView setTranslatesAutoresizingMaskIntoConstraints:NO];
[NSLayoutConstraint activateConstraints:@[
[[stackView topAnchor] constraintEqualToAnchor:[contentView topAnchor]],
[[stackView leadingAnchor] constraintEqualToAnchor:[contentView leadingAnchor]],
[[stackView bottomAnchor] constraintEqualToAnchor:[contentView bottomAnchor]],
[[stackView trailingAnchor] constraintEqualToAnchor:[contentView trailingAnchor]]
]];
#if PLATFORM(MAC)
[window makeKeyAndOrderFront:nil];
#else
[hostWindow setHidden:NO];
[hostWindow setNeedsLayout];
[hostWindow layoutIfNeeded];
#endif
EXPECT_TRUE(CGRectEqualToRect([webView frame], windowRect));
}
TEST(ScreenTime, URLIsPlayingVideo)
{
__block bool childRestrictionsDone = false;
RetainPtr webView = webViewForScreenTimeTests();
RetainPtr contentHTML = @"<!DOCTYPE html><html><head></head><body><video src=\"video-with-audio.mp4\" webkit-playsinline></video></body></html>";
TestWebKitAPI::HTTPServer server({
{ "/"_s, { contentHTML.get() } },
{ "/favicon.ico"_s, { "Actual response is immaterial."_s } },
{ "/video-with-audio.mp4"_s, [NSData dataWithContentsOfURL:[NSBundle.test_resourcesBundle URLForResource:@"video-with-audio" withExtension:@"mp4"]] },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadRequest:server.requestWithLocalhost()];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView objectByEvaluatingJavaScript:@"function eventToMessage(event){window.webkit.messageHandlers.testHandler.postMessage(event.type);} var video = document.querySelector('video'); video.addEventListener('playing', eventToMessage); video.addEventListener('pause', eventToMessage);"];
__block bool didBeginPlaying = false;
[webView performAfterReceivingMessage:@"playing" action:^{ didBeginPlaying = true; }];
[webView evaluateJavaScript:@"document.querySelector('video').play()" completionHandler:nil];
TestWebKitAPI::Util::run(&didBeginPlaying);
EXPECT_TRUE([[webView _screenTimeWebpageController] URLIsPlayingVideo]);
__block bool didPause = false;
[webView performAfterReceivingMessage:@"pause" action:^{ didPause = true; }];
[webView evaluateJavaScript:@"document.querySelector('video').pause()" completionHandler:nil];
TestWebKitAPI::Util::run(&didPause);
EXPECT_FALSE([[webView _screenTimeWebpageController] URLIsPlayingVideo]);
}
#if PLATFORM(MAC)
@interface STPictureInPictureUIDelegate : NSObject <WKUIDelegate, WKScriptMessageHandler>
@end
@implementation STPictureInPictureUIDelegate
- (void)_webView:(WKWebView *)webView hasVideoInPictureInPictureDidChange:(BOOL)hasVideoInPictureInPicture
{
hasVideoInPictureInPictureValue = hasVideoInPictureInPicture;
hasVideoInPictureInPictureCalled = true;
}
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
NSString *bodyString = (NSString *)[message body];
if ([bodyString isEqualToString:@"load"])
receivedLoadMessage = true;
}
@end
TEST(ScreenTime, URLIsPictureInPicture)
{
__block bool childRestrictionsDone = false;
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration preferences]._allowsPictureInPictureMediaPlayback = YES;
RetainPtr handler = adoptNS([[STPictureInPictureUIDelegate alloc] init]);
[[configuration userContentController] addScriptMessageHandler:handler.get() name:@"pictureInPictureChangeHandler"];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
[webView setFrame:NSMakeRect(0, 0, 640, 480)];
[webView setUIDelegate:handler.get()];
[webView _forceRequestCandidates];
RetainPtr window = adoptNS([[NSWindow alloc] initWithContentRect:[webView frame] styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]);
[[window contentView] addSubview:webView.get()];
[window makeKeyAndOrderFront:nil];
RetainPtr contentHTML = [NSString stringWithContentsOfFile:[NSBundle.test_resourcesBundle pathForResource:@"PictureInPictureDelegate" ofType:@"html"] encoding:NSUTF8StringEncoding error:NULL];
TestWebKitAPI::HTTPServer server({
{ "/"_s, { contentHTML.get() } },
{ "/favicon.ico"_s, { "Actual response is immaterial."_s } },
{ "/test.mp4"_s, [NSData dataWithContentsOfURL:[NSBundle.test_resourcesBundle URLForResource:@"test" withExtension:@"mp4"]] },
}, TestWebKitAPI::HTTPServer::Protocol::Http);
receivedLoadMessage = false;
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView loadRequest:server.requestWithLocalhost()];
TestWebKitAPI::Util::run(&receivedLoadMessage);
TestWebKitAPI::Util::run(&childRestrictionsDone);
hasVideoInPictureInPictureValue = false;
hasVideoInPictureInPictureCalled = false;
while (![webView _canTogglePictureInPicture])
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:0.05]];
ASSERT_FALSE([webView _isPictureInPictureActive]);
[webView _togglePictureInPicture];
TestWebKitAPI::Util::run(&hasVideoInPictureInPictureCalled);
EXPECT_TRUE(hasVideoInPictureInPictureValue);
EXPECT_TRUE([[webView _screenTimeWebpageController] URLIsPictureInPicture]);
// Wait for PIPAgent to launch, or it won't call -pipDidClose: callback.
[[NSRunLoop currentRunLoop] runUntilDate:[NSDate dateWithTimeIntervalSinceNow:1]];
ASSERT_TRUE([webView _isPictureInPictureActive]);
ASSERT_TRUE([webView _canTogglePictureInPicture]);
hasVideoInPictureInPictureCalled = false;
[webView _togglePictureInPicture];
TestWebKitAPI::Util::run(&hasVideoInPictureInPictureCalled);
EXPECT_FALSE(hasVideoInPictureInPictureValue);
EXPECT_FALSE([[webView _screenTimeWebpageController] URLIsPictureInPicture]);
}
TEST(ScreenTime, WebContentIsNotClickableBehindSystemScreenTimeBlockingView)
{
testWebContentIsNotClickableShowingSystemScreenTimeBlockingView(true);
}
TEST(ScreenTime, WebContentIsNotClickableBehindBlurredBlockingView)
{
testWebContentIsNotClickableShowingSystemScreenTimeBlockingView(false);
}
#endif
TEST(ScreenTime, FetchData)
{
__block bool childRestrictionsDone = false;
__block RetainPtr<NSSet<NSURL *>> urls;
InstanceMethodSwizzler swizzler {
PAL::getSTWebHistoryClassSingleton(),
@selector(fetchAllHistoryWithCompletionHandler:),
imp_implementationWithBlock(^(id object, void (^completionHandler)(NSSet<NSURL *> *urls, NSError *error)) {
urls = [NSSet setWithArray:@[ adoptNS([[NSURL alloc] initWithString:@"https://www.webkit.org/"]).get() ]];
completionHandler(urls.get(), nil);
})
};
RetainPtr dataTypeScreenTime = adoptNS([[NSSet alloc] initWithArray:@[ WKWebsiteDataTypeScreenTime ]]);
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
RetainPtr websiteDataStore = [WKWebsiteDataStore defaultDataStore];
[configuration setWebsiteDataStore:websiteDataStore.get()];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
__block bool done = false;
[websiteDataStore fetchDataRecordsOfTypes:dataTypeScreenTime.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) {
EXPECT_WK_STREQ([[dataRecords firstObject] displayName], "webkit.org");
done = true;
}];
TestWebKitAPI::Util::run(&done);
}
TEST(ScreenTime, RemoveDataWithTimeInterval)
{
__block bool childRestrictionsDone = false;
__block bool removedHistory = false;
InstanceMethodSwizzler swizzler {
PAL::getSTWebHistoryClassSingleton(),
@selector(deleteHistoryDuringInterval:),
imp_implementationWithBlock(^(id object, NSDateInterval *interval) {
removedHistory = true;
})
};
RetainPtr dataTypeScreenTime = adoptNS([[NSSet alloc] initWithArray:@[ WKWebsiteDataTypeScreenTime ]]);
RetainPtr uuid = [NSUUID UUID];
RetainPtr websiteDataStore = [WKWebsiteDataStore dataStoreForIdentifier:uuid.get()];
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration setWebsiteDataStore:websiteDataStore.get()];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
__block bool done = false;
[websiteDataStore removeDataOfTypes:dataTypeScreenTime.get() modifiedSince:[NSDate distantPast] completionHandler:^() {
done = true;
}];
TestWebKitAPI::Util::run(&done);
EXPECT_TRUE(removedHistory);
}
TEST(ScreenTime, RemoveData)
{
__block bool childRestrictionsDone = false;
__block RetainPtr<NSSet<NSURL *>> fetchedURLs = adoptNS([[NSSet alloc] initWithArray:@[
adoptNS([[NSURL alloc] initWithString:@"https://www.github.com/WebKit/WebKit"]).get(),
adoptNS([[NSURL alloc] initWithString:@"https://www.github.com/APPLE"]).get(),
adoptNS([[NSURL alloc] initWithString:@"https://fonts.github.com/"]).get(),
adoptNS([[NSURL alloc] initWithString:@"https://abcdefg.github.com/aPPLe/abc"]).get()
]]);
InstanceMethodSwizzler fetchHistorySwizzler {
PAL::getSTWebHistoryClassSingleton(),
@selector(fetchAllHistoryWithCompletionHandler:),
imp_implementationWithBlock(^(id object, void (^completionHandler)(NSSet<NSURL *> *urls, NSError *error)) {
completionHandler(fetchedURLs.get(), nil);
})
};
__block RetainPtr<NSMutableSet<NSURL *>> deletedURLs = adoptNS([[NSMutableSet alloc] init]);
InstanceMethodSwizzler deleteHistorySwizzler {
PAL::getSTWebHistoryClassSingleton(),
@selector(deleteHistoryForURL:),
imp_implementationWithBlock(^(id object, NSURL *url) {
[deletedURLs addObject:url];
})
};
RetainPtr dataTypeScreenTime = adoptNS([[NSSet alloc] initWithArray:@[ WKWebsiteDataTypeScreenTime ]]);
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
RetainPtr websiteDataStore = [WKWebsiteDataStore defaultDataStore];
[configuration setWebsiteDataStore:websiteDataStore.get()];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://www.github.com/WebKit/WebKit"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
__block bool done = false;
[websiteDataStore fetchDataRecordsOfTypes:dataTypeScreenTime.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) {
[websiteDataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] forDataRecords:dataRecords completionHandler:^{
done = true;
}];
}];
TestWebKitAPI::Util::run(&done);
EXPECT_EQ([deletedURLs count], [fetchedURLs count]);
for (NSURL *url in fetchedURLs.get())
EXPECT_TRUE([deletedURLs containsObject:url]);
}
TEST(ScreenTime, OffscreenSystemScreenTimeBlockingView)
{
__block bool childRestrictionsDone = false;
RetainPtr webView = webViewForScreenTimeTests();
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
EXPECT_FALSE([[[webView _screenTimeWebpageController] view] isHidden]);
[webView removeFromTestWindow];
[webView waitUntilActivityStateUpdateDone];
EXPECT_TRUE([[[webView _screenTimeWebpageController] view] isHidden]);
[webView addToTestWindow];
EXPECT_FALSE([[[webView _screenTimeWebpageController] view] isHidden]);
}
TEST(ScreenTime, OffscreenBlurredScreenTimeBlockingView)
{
__block bool childRestrictionsDone = false;
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration setShowsSystemScreenTimeBlockingView:NO];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
RetainPtr controller = [webView _screenTimeWebpageController];
[controller setURLIsBlocked:YES];
EXPECT_TRUE(blurredViewIsPresent(webView.get()));
[webView removeFromTestWindow];
[webView waitUntilActivityStateUpdateDone];
[webView waitForNextPresentationUpdate];
EXPECT_TRUE([[webView _screenTimeBlurredSnapshot] isHidden]);
[webView addToTestWindow];
[webView waitUntilActivityStateUpdateDone];
[webView waitForNextPresentationUpdate];
EXPECT_TRUE(blurredViewIsPresent(webView.get()));
EXPECT_FALSE([[webView _screenTimeBlurredSnapshot] isHidden]);
}
#if PLATFORM(MAC)
TEST(ScreenTime, DoNotDonateURLsInOccludedWebView)
{
// FIXME: This test will fail if running on a full screen terminal window. It passes otherwise.
__block bool suppressUsageRecording = false;
__block bool done = false;
__block bool childRestrictionsDone = false;
InstanceMethodSwizzler swizzler {
PAL::getSTWebpageControllerClassSingleton(),
@selector(setSuppressUsageRecording:),
imp_implementationWithBlock(^(id object, bool value) {
suppressUsageRecording = value;
done = true;
})
};
RetainPtr webView = webViewForScreenTimeTests();
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
RetainPtr controller = [webView _screenTimeWebpageController];
[controller setURLIsBlocked:YES];
TestWebKitAPI::Util::run(&done);
EXPECT_FALSE(suppressUsageRecording);
suppressUsageRecording = false;
done = false;
RetainPtr window = adoptNS([[NSWindow alloc] initWithContentRect:[webView frame] styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]);
[window setIsVisible:YES];
[window makeKeyAndOrderFront:nil];
TestWebKitAPI::Util::run(&done);
EXPECT_TRUE(suppressUsageRecording);
suppressUsageRecording = false;
done = false;
[window setFrame:CGRectZero display:YES];
TestWebKitAPI::Util::run(&done);
EXPECT_FALSE(suppressUsageRecording);
}
TEST(ScreenTime, FullscreenWithSystemBlockingViewInHierarchy)
{
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration preferences].elementFullscreenEnabled = YES;
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
__block bool childRestrictionsDone = false;
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@"<div id=\"target\" style=\"width:150px;height:150px;background-color:red\"></div>"];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
EXPECT_TRUE(systemScreenTimeBlockingViewIsPresent(webView.get()));
RetainPtr controller = [webView _screenTimeWebpageController];
Class remoteViewClass = NSClassFromString(@"NSRemoteView");
TestWebKitAPI::Util::waitFor([&] {
return [[[controller view] subviews] indexOfObjectPassingTest:^BOOL(NSView *subview, NSUInteger, BOOL *) {
return [subview isKindOfClass:remoteViewClass];
}] != NSNotFound;
});
if ([webView _wantsConstraintBasedLayout]) {
[webView setTranslatesAutoresizingMaskIntoConstraints:NO];
RetainPtr superview = [webView superview];
[NSLayoutConstraint activateConstraints:@[
[[webView topAnchor] constraintEqualToAnchor:[superview topAnchor]],
[[webView leadingAnchor] constraintEqualToAnchor:[superview leadingAnchor]],
[[webView bottomAnchor] constraintEqualToAnchor:[superview bottomAnchor]],
[[webView trailingAnchor] constraintEqualToAnchor:[superview trailingAnchor]]
]];
}
[webView objectByEvaluatingJavaScript:@"document.querySelector('#target').addEventListener('fullscreenchange', event => { window.webkit.messageHandlers.testHandler.postMessage('fullscreenchange'); });"];
bool receivedFullscreenChangeMessage = false;
[webView performAfterReceivingMessage:@"fullscreenchange" action:[&] {
receivedFullscreenChangeMessage = true;
}];
RetainPtr originalWindow = [webView window];
[webView objectByEvaluatingJavaScriptWithUserGesture:@"document.querySelector('#target').webkitRequestFullscreen()"];
TestWebKitAPI::Util::run(&receivedFullscreenChangeMessage);
receivedFullscreenChangeMessage = false;
TestWebKitAPI::Util::waitFor([&] {
RetainPtr window = [webView window];
return [window isVisible] && (window != originalWindow);
});
NSSize fullscreenWindowSize = [[webView window] frame].size;
EXPECT_EQ([webView frame], NSMakeRect(0, 0, fullscreenWindowSize.width, fullscreenWindowSize.height));
}
#endif
TEST(ScreenTime, CreateControllerAfterOffscreenWebViewBecomesInWindow)
{
__block bool childRestrictionsDone = false;
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
RetainPtr webView = webViewForScreenTimeTests(configuration.get(), NO);
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadHTMLString: @"" baseURL:[NSURL URLWithString:@"http://webkit.org"]];
[webView waitForNextPresentationUpdate];
EXPECT_FALSE(!![webView _screenTimeWebpageController]);
[webView addToTestWindow];
TestWebKitAPI::Util::run(&childRestrictionsDone);
TestWebKitAPI::Util::waitFor([&] {
return !![webView _screenTimeWebpageController];
});
EXPECT_TRUE(!![webView _screenTimeWebpageController]);
}
TEST(ScreenTime, ScreenTimeControllerSetsURLWhenOffscreenWebViewBecomesInWindow)
{
__block bool childRestrictionsDone = false;
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
RetainPtr webView = webViewForScreenTimeTests(configuration.get(), NO);
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
[webView waitForNextPresentationUpdate];
EXPECT_FALSE(!![webView _screenTimeWebpageController]);
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView addToTestWindow];
TestWebKitAPI::Util::run(&childRestrictionsDone);
TestWebKitAPI::Util::waitFor([&] {
return !![webView _screenTimeWebpageController];
});
EXPECT_TRUE(!![webView _screenTimeWebpageController]);
TestWebKitAPI::Util::waitFor([&] {
return !![[webView _screenTimeWebpageController] URL];
});
EXPECT_NOT_NULL([[webView _screenTimeWebpageController] URL]);
}
TEST(ScreenTime, ScreenTimeControllerInstalledAfterRestoreFromSessionState)
{
__block bool childRestrictionsDone = false;
RetainPtr webView1 = webViewForScreenTimeTests();
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView1 synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
RetainPtr sessionState = [webView1 _sessionState];
[webView1 _close];
RetainPtr webView2 = webViewForScreenTimeTests(nil, NO);
EXPECT_FALSE(!![webView2 _screenTimeWebpageController]);
[webView2 addToTestWindow];
[webView2 _restoreSessionState:sessionState.get() andNavigate:YES];
[webView2 _test_waitForDidFinishNavigation];
[webView2 waitForNextPresentationUpdate];
EXPECT_TRUE(!![webView2 _screenTimeWebpageController]);
}
TEST(ScreenTime, ScreenTimeControllerViewOnlyInstalledForHTTPFamily)
{
__block bool childRestrictionsDone = false;
RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]);
[configuration setShowsSystemScreenTimeBlockingView:YES];
RetainPtr webView = webViewForScreenTimeTests(configuration.get());
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@""]];
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone);
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
[webView waitForNextPresentationUpdate];
EXPECT_FALSE(systemScreenTimeBlockingViewIsPresent(webView.get()));
[webView synchronouslyLoadHTMLString:@"<style> body { background-color: red; } </style>"];
[webView waitForNextPresentationUpdate];
EXPECT_FALSE(systemScreenTimeBlockingViewIsPresent(webView.get()));
request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
TestWebKitAPI::Util::run(&childRestrictionsDone);
[webView waitForNextPresentationUpdate];
EXPECT_TRUE(systemScreenTimeBlockingViewIsPresent(webView.get()));
}
TEST(ScreenTime, ScreenTimeControllerNotInstalledForNoChildRestrictions)
{
__block bool childRestrictionsDone = false;
RetainPtr webView = webViewForScreenTimeTests();
auto swizzle = swizzleEnforcesChildRestrictions(childRestrictionsDone, NO);
RetainPtr request = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]];
[webView synchronouslyLoadSimulatedRequest:request.get() responseHTMLString:@""];
[webView waitForNextPresentationUpdate];
EXPECT_FALSE(!![webView _screenTimeWebpageController]);
}
#endif