/* * Copyright (C) 2020 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 "PlatformUtilities.h" #import "TestNavigationDelegate.h" #import "TestWKWebView.h" #import #import #import #import #import TEST(ProcessSuspension, CancelWebProcessSuspension) { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get() addToWindow:YES]); [webView synchronouslyLoadTestPageNamed:@"large-red-square-image"]; auto pid1 = [webView _webProcessIdentifier]; EXPECT_NE(pid1, 0); [webView _processWillSuspendImminentlyForTesting]; [webView _processDidResumeForTesting]; bool done = false; [webView evaluateJavaScript:@"window.location" completionHandler: [&] (id result, NSError *error) { auto pid2 = [webView _webProcessIdentifier]; EXPECT_EQ(pid1, pid2); done = true; }]; TestWebKitAPI::Util::run(&done); } TEST(ProcessSuspension, DestroyWebPageDuringWebProcessSuspension) { auto configuration1 = adoptNS([[WKWebViewConfiguration alloc] init]); auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 100, 100) configuration:configuration1.get() addToWindow:YES]); [webView1 synchronouslyLoadTestPageNamed:@"large-red-square-image"]; auto pid1 = [webView1 _webProcessIdentifier]; EXPECT_NE(pid1, 0); auto configuration2 = adoptNS([[WKWebViewConfiguration alloc] init]); configuration2.get().processPool = configuration1.get().processPool; configuration2.get()._relatedWebView = webView1.get(); auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(100, 0, 100, 100) configuration:configuration2.get() addToWindow:YES]); [webView2 synchronouslyLoadTestPageNamed:@"large-red-square-image"]; auto pid2 = [webView2 _webProcessIdentifier]; EXPECT_EQ(pid1, pid2); auto webView3 = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(200, 0, 100, 100) configuration:configuration2.get() addToWindow:YES]); [webView3 synchronouslyLoadTestPageNamed:@"large-red-square-image"]; [webView3 _processWillSuspendForTesting:^{ }]; [webView1 _close]; TestWebKitAPI::Util::sleep(0.1); [webView2 _close]; EXPECT_EQ(pid1, [webView3 _webProcessIdentifier]); TestWebKitAPI::Util::sleep(1); EXPECT_EQ(pid1, [webView3 _webProcessIdentifier]); } TEST(ProcessSuspension, WKWebViewSuspendPage) { auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); auto testMessageHandler = adoptNS([[TestMessageHandler alloc] init]); [[configuration userContentController] addScriptMessageHandler:testMessageHandler.get() name:@"testHandler"]; __block unsigned timerFiredCount = 0; [testMessageHandler addMessage:@"timer fired" withHandler:^{ ++timerFiredCount; }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get() addToWindow:YES]); [webView synchronouslyLoadTestPageNamed:@"postMessage-regularly"]; auto pid = [webView _webProcessIdentifier]; EXPECT_NE(pid, 0); // The timer should be firing. auto lastTimerFireCount = timerFiredCount; while (lastTimerFireCount == timerFiredCount) TestWebKitAPI::Util::spinRunLoop(10); EXPECT_TRUE(timerFiredCount > lastTimerFireCount); auto testRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"https://webkit.org/"]]; id testInteractionState = [webView interactionState]; auto originalURL = [webView URL]; // Suspend the page. [webView removeFromSuperview]; __block bool done = false; [webView _suspendPage:^(BOOL success) { EXPECT_TRUE(success); done = true; }]; TestWebKitAPI::Util::run(&done); // The timer should no longer be firing. lastTimerFireCount = timerFiredCount; TestWebKitAPI::Util::spinRunLoop(100); EXPECT_EQ(lastTimerFireCount, timerFiredCount); auto checkThrows = [](Function&& f) { @try { f(); } @catch (NSException *e) { return true; } return false; }; // Most operations on WKWebView should throw an exception when suspended. EXPECT_TRUE(checkThrows([&] { [webView evaluateJavaScript:@"window.name" completionHandler:nil]; })); EXPECT_TRUE(checkThrows([&] { [webView synchronouslyLoadTestPageNamed:@"simple2"]; })); EXPECT_TRUE(checkThrows([&] { [webView loadHTMLString:@"foo" baseURL:nil]; })); EXPECT_TRUE(checkThrows([&] { [webView goToBackForwardListItem:webView.get().backForwardList.currentItem]; })); EXPECT_TRUE(checkThrows([&] { [webView reload]; })); EXPECT_TRUE(checkThrows([&] { [webView reloadFromOrigin]; })); EXPECT_TRUE(checkThrows([&] { [webView goBack]; })); EXPECT_TRUE(checkThrows([&] { [webView goForward]; })); EXPECT_TRUE(checkThrows([&] { [webView stopLoading]; })); EXPECT_TRUE(checkThrows([&] { [webView closeAllMediaPresentationsWithCompletionHandler:nil]; })); EXPECT_TRUE(checkThrows([&] { [webView pauseAllMediaPlaybackWithCompletionHandler:nil]; })); EXPECT_TRUE(checkThrows([&] { [webView setAllMediaPlaybackSuspended:YES completionHandler:nil]; })); EXPECT_TRUE(checkThrows([&] { [webView requestMediaPlaybackStateWithCompletionHandler:^(WKMediaPlaybackState) { }]; })); EXPECT_TRUE(checkThrows([&] { [webView setCameraCaptureState:WKMediaCaptureStateActive completionHandler:nil]; })); EXPECT_TRUE(checkThrows([&] { [webView setMicrophoneCaptureState:WKMediaCaptureStateActive completionHandler:nil]; })); #if TARGET_OS_IPHONE EXPECT_TRUE(checkThrows([&] { [webView takeSnapshotWithConfiguration:nil completionHandler:^(UIImage *snapshot, NSError *error) { }]; })); #else EXPECT_TRUE(checkThrows([&] { [webView takeSnapshotWithConfiguration:nil completionHandler:^(NSImage *snapshot, NSError *error) { }]; })); #endif EXPECT_TRUE(checkThrows([&] { [webView createPDFWithConfiguration:nil completionHandler:^(NSData *data, NSError *error) { }]; })); EXPECT_TRUE(checkThrows([&] { [webView createWebArchiveDataWithCompletionHandler:^(NSData *data, NSError *error) { }]; })); EXPECT_TRUE(checkThrows([&] { webView.get().allowsBackForwardNavigationGestures = YES; })); EXPECT_TRUE(checkThrows([&] { webView.get().customUserAgent = @"foo"; })); EXPECT_TRUE(checkThrows([&] { webView.get().allowsLinkPreview = YES; })); #if !TARGET_OS_IPHONE EXPECT_TRUE(checkThrows([&] { webView.get().allowsMagnification = YES; })); EXPECT_TRUE(checkThrows([&] { webView.get().magnification = 1.1; })); EXPECT_TRUE(checkThrows([&] { [webView setMagnification:1.1 centeredAtPoint:CGPointMake(0, 0)]; })); #endif EXPECT_TRUE(checkThrows([&] { webView.get().pageZoom = 1.1; })); EXPECT_TRUE(checkThrows([&] { [webView findString:@"foo" withConfiguration:nil completionHandler:^(WKFindResult *result) { }]; })); EXPECT_TRUE(checkThrows([&] { [webView startDownloadUsingRequest:testRequest completionHandler:^(WKDownload *download) { }]; })); EXPECT_TRUE(checkThrows([&] { webView.get().interactionState = testInteractionState; })); EXPECT_TRUE(checkThrows([&] { webView.get().mediaType = @"text/html"; })); // Some of the basic API still works. EXPECT_WK_STREQ([webView URL].absoluteString, originalURL.absoluteString); EXPECT_EQ([webView backForwardList].backItem, nil); EXPECT_WK_STREQ([webView title], @""); EXPECT_FALSE([webView isLoading]); EXPECT_EQ([webView estimatedProgress], 1.0); EXPECT_FALSE([webView canGoBack]); EXPECT_FALSE([webView canGoForward]); EXPECT_FALSE([webView hasOnlySecureContent]); EXPECT_EQ([webView serverTrust], nil); EXPECT_EQ([webView cameraCaptureState], WKMediaCaptureStateNone); EXPECT_EQ([webView microphoneCaptureState], WKMediaCaptureStateNone); EXPECT_WK_STREQ([webView customUserAgent], @""); EXPECT_TRUE([webView allowsLinkPreview]); #if !TARGET_OS_IPHONE EXPECT_FALSE([webView allowsMagnification]); EXPECT_EQ([webView magnification], 1.0); #endif EXPECT_EQ([webView pageZoom], 1.0); EXPECT_WK_STREQ([webView mediaType], @""); EXPECT_FALSE(checkThrows([&] { [webView interactionState]; })); EXPECT_FALSE(checkThrows([&] { [webView themeColor]; })); // Resume the page. done = false; [webView _resumePage:^(BOOL success) { EXPECT_TRUE(success); done = true; }]; TestWebKitAPI::Util::run(&done); [webView addToTestWindow]; // The timer should start firing again. lastTimerFireCount = timerFiredCount; while (lastTimerFireCount == timerFiredCount) TestWebKitAPI::Util::spinRunLoop(10); EXPECT_TRUE(timerFiredCount > lastTimerFireCount); // Make sure we did not crash. EXPECT_EQ(pid, [webView _webProcessIdentifier]); } TEST(ProcessSuspension, DeallocateSuspendedView) { // Deallocating a suspended WebView should not throw or crash. @autoreleasepool { auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600)]); [webView loadRequest:[NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"simple" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]]; [webView _test_waitForDidFinishNavigation]; __block bool done = false; [webView _suspendPage:^(BOOL success) { EXPECT_TRUE(success); done = true; }]; TestWebKitAPI::Util::run(&done); [webView _close]; } }