| /* |
| * Copyright (C) 2017-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" |
| |
| #import "DeprecatedGlobalValues.h" |
| #import "HTTPServer.h" |
| #import "PlatformUtilities.h" |
| #import "ServiceWorkerPageProtocol.h" |
| #import "Test.h" |
| #import "TestDownloadDelegate.h" |
| #import "TestNavigationDelegate.h" |
| #import "TestUIDelegate.h" |
| #import "TestWKWebView.h" |
| #import "WKWebViewConfigurationExtras.h" |
| #import <WebCore/CertificateInfo.h> |
| #import <WebKit/WKContextPrivate.h> |
| #import <WebKit/WKPreferencesPrivate.h> |
| #import <WebKit/WKProcessPoolPrivate.h> |
| #import <WebKit/WKURLSchemeHandler.h> |
| #import <WebKit/WKURLSchemeTaskPrivate.h> |
| #import <WebKit/WKUserContentControllerPrivate.h> |
| #import <WebKit/WKUserScript.h> |
| #import <WebKit/WKUserScriptPrivate.h> |
| #import <WebKit/WKWebViewConfigurationPrivate.h> |
| #import <WebKit/WKWebViewPrivateForTesting.h> |
| #import <WebKit/WKWebpagePreferencesPrivate.h> |
| #import <WebKit/WKWebsiteDataStorePrivate.h> |
| #import <WebKit/WKWebsiteDataStoreRef.h> |
| #import <WebKit/WebKit.h> |
| #import <WebKit/_WKFeature.h> |
| #import <WebKit/_WKProcessPoolConfiguration.h> |
| #import <WebKit/_WKRemoteObjectInterface.h> |
| #import <WebKit/_WKRemoteObjectRegistry.h> |
| #import <WebKit/_WKWebsiteDataStoreConfiguration.h> |
| #import <WebKit/_WKWebsiteDataStoreDelegate.h> |
| #import <mach/mach_init.h> |
| #import <mach/task.h> |
| #import <mach/task_info.h> |
| #import <wtf/BlockPtr.h> |
| #import <wtf/Deque.h> |
| #import <wtf/FileSystem.h> |
| #import <wtf/HashMap.h> |
| #import <wtf/RetainPtr.h> |
| #import <wtf/Scope.h> |
| #import <wtf/StdLibExtras.h> |
| #import <wtf/URL.h> |
| #import <wtf/Vector.h> |
| #import <wtf/cocoa/SpanCocoa.h> |
| #import <wtf/cocoa/TypeCastsCocoa.h> |
| #import <wtf/darwin/DispatchExtras.h> |
| #import <wtf/text/MakeString.h> |
| #import <wtf/text/StringHash.h> |
| #import <wtf/text/WTFString.h> |
| |
| static NSString *serviceWorkerRegistrationFilename = @"ServiceWorkerRegistrations-8.sqlite3"; |
| static bool serviceWorkerGlobalObjectIsAvailable; |
| |
| static String expectedMessage; |
| static String retrievedString; |
| |
| @interface SWMessageHandler : NSObject <WKScriptMessageHandler> |
| @end |
| |
| @implementation SWMessageHandler |
| - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
| { |
| EXPECT_WK_STREQ(@"Message from worker: ServiceWorker received: Hello from the web page", [message body]); |
| done = true; |
| } |
| @end |
| |
| @interface SWMessageHandlerForFetchTest : NSObject <WKScriptMessageHandler> |
| @end |
| |
| @implementation SWMessageHandlerForFetchTest |
| - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
| { |
| EXPECT_TRUE([[message body] isEqualToString:@"Intercepted by worker"]); |
| done = true; |
| } |
| @end |
| |
| @interface SWMessageHandlerForRestoreFromDiskTest : NSObject <WKScriptMessageHandler> { |
| NSString *_expectedMessage; |
| } |
| - (instancetype)initWithExpectedMessage:(NSString *)expectedMessage; |
| - (void)resetExpectedMessage:(NSString *)expectedMessage; |
| @end |
| |
| @interface SWMessageHandlerWithExpectedMessage : NSObject <WKScriptMessageHandler> |
| @end |
| |
| @implementation SWMessageHandlerWithExpectedMessage |
| - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
| { |
| EXPECT_WK_STREQ(message.body, expectedMessage); |
| done = true; |
| } |
| @end |
| |
| @implementation SWMessageHandlerForRestoreFromDiskTest |
| |
| - (instancetype)initWithExpectedMessage:(NSString *)expectedMessage |
| { |
| if (!(self = [super init])) |
| return nil; |
| |
| _expectedMessage = expectedMessage; |
| |
| return self; |
| } |
| |
| - (void)resetExpectedMessage:(NSString *)expectedMessage |
| { |
| _expectedMessage = expectedMessage; |
| } |
| |
| - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
| { |
| EXPECT_WK_STREQ(message.body, _expectedMessage); |
| done = true; |
| } |
| @end |
| |
| static bool shouldAccept = true; |
| static bool navigationComplete = false; |
| static bool navigationFailed = false; |
| |
| @interface TestSWAsyncNavigationDelegate : NSObject <WKNavigationDelegate, WKUIDelegate> |
| @end |
| |
| @implementation TestSWAsyncNavigationDelegate |
| |
| - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation |
| { |
| navigationComplete = true; |
| } |
| |
| - (void)webView:(WKWebView *)webView didFailNavigation:(WKNavigation *)navigation withError:(NSError *)error |
| { |
| navigationFailed = true; |
| navigationComplete = true; |
| } |
| |
| - (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error |
| { |
| navigationFailed = true; |
| navigationComplete = true; |
| } |
| |
| - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler |
| { |
| decisionHandler(WKNavigationActionPolicyAllow); |
| } |
| |
| - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler |
| { |
| int64_t deferredWaitTime = 100 * NSEC_PER_MSEC; |
| dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, deferredWaitTime); |
| dispatch_after(when, mainDispatchQueueSingleton(), ^{ |
| decisionHandler(shouldAccept ? WKNavigationResponsePolicyAllow : WKNavigationResponsePolicyCancel); |
| }); |
| } |
| @end |
| |
| static constexpr auto mainCacheStorageBytes = R"SWRESOURCE( |
| <script> |
| |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| async function doTest() |
| { |
| const keys = await window.caches.keys(); |
| if (!keys.length) { |
| const cache = await window.caches.open("my cache"); |
| log("No cache storage data"); |
| return; |
| } |
| if (keys.length !== 1) { |
| log("Unexpected cache number"); |
| return; |
| } |
| log("Some cache storage data: " + keys[0]); |
| } |
| doTest(); |
| |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto mainBytes = R"SWRESOURCE( |
| <script> |
| |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| navigator.serviceWorker.addEventListener("message", function(event) { |
| log("Message from worker: " + event.data); |
| }); |
| |
| try { |
| |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { |
| worker = reg.installing ? reg.installing : reg.active; |
| worker.postMessage("Hello from the web page"); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto scriptBytes = R"SWRESOURCE( |
| |
| self.addEventListener("message", (event) => { |
| event.source.postMessage("ServiceWorker received: " + event.data); |
| }); |
| |
| )SWRESOURCE"_s; |
| |
| static constexpr auto scriptWithEvalBytes = R"SWRESOURCE( |
| |
| self.addEventListener("message", (event) => { |
| if (event.data == "Hello from the web page") { |
| event.source.postMessage("ServiceWorker received: " + event.data); |
| return; |
| } |
| event.source.postMessage("Evaluation result: " + eval(event.data)); |
| }); |
| |
| )SWRESOURCE"_s; |
| |
| static constexpr auto mainForFetchTestBytes = R"SWRESOURCE( |
| <html> |
| <body> |
| <script> |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| try { |
| |
| function addFrame() |
| { |
| frame = document.createElement('iframe'); |
| frame.src = "/test.html"; |
| frame.onload = function() { window.webkit.messageHandlers.sw.postMessage(frame.contentDocument.body.innerHTML); } |
| document.body.appendChild(frame); |
| } |
| |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { |
| if (reg.active) { |
| addFrame(); |
| return; |
| } |
| worker = reg.installing; |
| worker.addEventListener('statechange', function() { |
| if (worker.state == 'activated') |
| addFrame(); |
| }); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| |
| </script> |
| </body> |
| </html> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto scriptHandlingFetchBytes = R"SWRESOURCE( |
| |
| self.addEventListener("fetch", (event) => { |
| if (event.request.url.indexOf("test.html") !== -1) { |
| event.respondWith(new Response(new Blob(['Intercepted by worker'], {type: 'text/html'}))); |
| } |
| }); |
| |
| )SWRESOURCE"_s; |
| |
| static constexpr auto scriptInterceptingFirstLoadBytes = R"SWRESOURCE( |
| |
| self.addEventListener("fetch", (event) => { |
| if (event.request.url.indexOf("main.html") !== -1) { |
| event.respondWith(new Response(new Blob(['Intercepted by worker <script>window.webkit.messageHandlers.sw.postMessage(\'Intercepted by worker\');</script>'], {type: 'text/html'}))); |
| } |
| }); |
| |
| )SWRESOURCE"_s; |
| |
| static constexpr auto mainForFirstLoadInterceptTestBytes = R"SWRESOURCE( |
| <html> |
| <body> |
| <script> |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| try { |
| |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { |
| if (reg.active) { |
| window.webkit.messageHandlers.sw.postMessage('Service Worker activated'); |
| return; |
| } |
| |
| worker = reg.installing; |
| worker.addEventListener('statechange', function() { |
| if (worker.state == 'activated') |
| window.webkit.messageHandlers.sw.postMessage('Service Worker activated'); |
| }); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| |
| </script> |
| </body> |
| </html> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto mainRegisteringWorkerBytes = R"SWRESOURCE( |
| <script> |
| try { |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { |
| if (reg.active) { |
| log("FAIL: Registration already has an active worker"); |
| return; |
| } |
| worker = reg.installing; |
| worker.addEventListener('statechange', function() { |
| if (worker.state == 'activated') |
| log("PASS: Registration was successful and service worker was activated"); |
| }); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto mainRegisteringAlreadyExistingWorkerBytes = R"SWRESOURCE( |
| <script> |
| try { |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { |
| if (reg.installing) { |
| log("FAIL: Registration had an installing worker"); |
| return; |
| } |
| if (reg.active) { |
| if (reg.active.state == "activated") |
| log("PASS: Registration already has an active worker"); |
| else |
| log("FAIL: Registration has an active worker but its state is not activated"); |
| } else |
| log("FAIL: Registration does not have an active worker"); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto mainBytesForSessionIDTest = R"SWRESOURCE( |
| <script> |
| |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| navigator.serviceWorker.addEventListener("message", function(event) { |
| log(event.data); |
| }); |
| |
| try { |
| |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { |
| worker = reg.installing; |
| worker.addEventListener('statechange', function() { |
| if (worker.state == 'activated') |
| worker.postMessage("TEST"); |
| }); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto scriptBytesForSessionIDTest = R"SWRESOURCE( |
| |
| var wasActivated = false; |
| |
| self.addEventListener("activate", event => { |
| event.waitUntil(clients.claim().then( () => { |
| wasActivated = true; |
| })); |
| }); |
| |
| self.addEventListener("message", (event) => { |
| if (wasActivated && registration.active) |
| event.source.postMessage("PASS: activation successful"); |
| else |
| event.source.postMessage("FAIL: failed to activate"); |
| }); |
| |
| )SWRESOURCE"_s; |
| |
| enum class ShouldRunServiceWorkersOnMainThread : bool { No, Yes }; |
| |
| static void setViewDataStore(WKWebViewConfiguration* viewConfiguration, ShouldRunServiceWorkersOnMainThread shouldRunServiceWorkersOnMainThread) |
| { |
| auto storeConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]); |
| [storeConfiguration setShouldRunServiceWorkersOnMainThreadForTesting:shouldRunServiceWorkersOnMainThread == ShouldRunServiceWorkersOnMainThread::Yes]; |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:storeConfiguration.get()]); |
| [viewConfiguration setWebsiteDataStore:dataStore.get()]; |
| } |
| |
| static void runBasicSWTest(ShouldRunServiceWorkersOnMainThread shouldRunServiceWorkersOnMainThread) |
| { |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setViewDataStore(configuration.get(), shouldRunServiceWorkersOnMainThread); |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| // Start with a clean slate data store |
| [[configuration websiteDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| webView = nullptr; |
| |
| [[configuration websiteDataStore] fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] completionHandler:^(NSArray<WKWebsiteDataRecord *> *websiteDataRecords) { |
| EXPECT_EQ(1u, [websiteDataRecords count]); |
| EXPECT_WK_STREQ(websiteDataRecords[0].displayName, "127.0.0.1"); |
| |
| done = true; |
| }]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, Basic) |
| { |
| runBasicSWTest(ShouldRunServiceWorkersOnMainThread::No); |
| } |
| |
| TEST(ServiceWorkers, BasicWithMainThreadSW) |
| { |
| runBasicSWTest(ShouldRunServiceWorkersOnMainThread::Yes); |
| } |
| |
| @interface SWCustomUserAgentDelegate : NSObject <WKNavigationDelegate> { |
| NSString *_userAgent; |
| } |
| - (instancetype)initWithUserAgent:(NSString *)userAgent; |
| @end |
| |
| @implementation SWCustomUserAgentDelegate |
| |
| - (instancetype)initWithUserAgent:(NSString *)userAgent |
| { |
| self = [super init]; |
| _userAgent = userAgent; |
| return self; |
| } |
| |
| - (void)_webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction preferences:(WKWebpagePreferences *)preferences userInfo:(id <NSSecureCoding>)userInfo decisionHandler:(void (^)(WKNavigationActionPolicy, WKWebpagePreferences *))decisionHandler |
| { |
| auto websitePolicies = adoptNS([[WKWebpagePreferences alloc] init]); |
| if (navigationAction.targetFrame.mainFrame) |
| [websitePolicies _setCustomUserAgent:_userAgent]; |
| |
| decisionHandler(WKNavigationActionPolicyAllow, websitePolicies.get()); |
| } |
| |
| @end |
| |
| @interface SWUserAgentMessageHandler : NSObject <WKScriptMessageHandler> { |
| @public |
| NSString *expectedMessage; |
| } |
| - (instancetype)initWithExpectedMessage:(NSString *)expectedMessage; |
| @end |
| |
| @implementation SWUserAgentMessageHandler |
| |
| - (instancetype)initWithExpectedMessage:(NSString *)_expectedMessage |
| { |
| self = [super init]; |
| expectedMessage = _expectedMessage; |
| return self; |
| } |
| |
| - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
| { |
| EXPECT_WK_STREQ(expectedMessage, [message body]); |
| done = true; |
| } |
| @end |
| |
| static constexpr auto userAgentSWBytes = R"SWRESOURCE( |
| |
| self.addEventListener("message", (event) => { |
| event.source.postMessage(navigator.userAgent); |
| }); |
| |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorkers, UserAgentOverride) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto messageHandler = adoptNS([[SWUserAgentMessageHandler alloc] initWithExpectedMessage:@"Message from worker: Foo Custom UserAgent"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, userAgentSWBytes } }, |
| }); |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| auto delegate = adoptNS([[SWCustomUserAgentDelegate alloc] initWithUserAgent:@"Foo Custom UserAgent"]); |
| [webView setNavigationDelegate:delegate.get()]; |
| |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| // Restore from disk. |
| webView = nullptr; |
| delegate = nullptr; |
| messageHandler = nullptr; |
| configuration = nullptr; |
| |
| configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| messageHandler = adoptNS([[SWUserAgentMessageHandler alloc] initWithExpectedMessage:@"Message from worker: Foo Custom UserAgent"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| delegate = adoptNS([[SWCustomUserAgentDelegate alloc] initWithUserAgent:@"Bar Custom UserAgent"]); |
| [webView setNavigationDelegate:delegate.get()]; |
| |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, RestoreFromDisk) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| RetainPtr<SWMessageHandlerForRestoreFromDiskTest> messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"PASS: Registration was successful and service worker was activated"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { mainRegisteringWorkerBytes } }, |
| { "/second.html"_s, { mainRegisteringAlreadyExistingWorkerBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/first.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| |
| webView = nullptr; |
| configuration = nullptr; |
| messageHandler = nullptr; |
| |
| done = false; |
| |
| configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"PASS: Registration already has an active worker"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/second.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| static constexpr auto scriptBytesWithFetchSupport = R"SWRESOURCE( |
| |
| self.addEventListener("message", (event) => { |
| if (event.data = 'do-fetch') { |
| fetch("foo.txt").then((response) => { |
| event.source.postMessage("Load succeeded"); |
| }).catch((err) => { |
| event.source.postMessage("Load failed"); |
| }); |
| } |
| }); |
| |
| )SWRESOURCE"_s; |
| |
| static constexpr auto mainRegisteringAlreadyExistingWorkerRequestFetchBytes = R"SWRESOURCE( |
| <script> |
| let activeServiceWorker = null; |
| try { |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| addEventListener("message", function(event) { |
| if (event.data === "do-fetch") { |
| if (activeServiceWorker) |
| activeServiceWorker.postMessage("do-fetch"); |
| else |
| log("FAIL: activeServiceWorker is null"); |
| } else |
| log("FAIL: unrecognized command: " + event.data); |
| }); |
| |
| navigator.serviceWorker.addEventListener("message", function(event) { |
| log("Message from worker: " + event.data); |
| }); |
| |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { |
| if (reg.installing) { |
| log("FAIL: Registration had an installing worker"); |
| return; |
| } |
| if (reg.active) { |
| if (reg.active.state == "activated") { |
| log("PASS: Registration already has an active worker"); |
| activeServiceWorker = reg.active; |
| } else |
| log("FAIL: Registration has an active worker but its state is not activated"); |
| } else |
| log("FAIL: Registration does not have an active worker"); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| </script> |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorkers, ThirdPartyRestoredFromDisk) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/index.html"_s, { "<script>onload = () => { webkit.messageHandlers.sw.postMessage('LOADED'); }</script>"_s } }, |
| { "/thirdPartyIframeWithSW.html"_s, { mainRegisteringWorkerBytes } }, |
| { "/thirdPartyIframeWithSW2.html"_s, { mainRegisteringAlreadyExistingWorkerRequestFetchBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytesWithFetchSupport } }, |
| { "/foo.txt"_s, { "FOO"_s } } |
| }); |
| |
| // Normally, service workers get terminated several seconds after their clients are gone. |
| // Disable this delay for the purpose of testing. |
| auto dataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| [dataStoreConfiguration setServiceWorkerProcessTerminationDelayEnabled:NO]; |
| |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); |
| |
| // Start with a clean slate data store |
| [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| configuration.get().websiteDataStore = dataStore.get(); |
| |
| RetainPtr<SWMessageHandlerForRestoreFromDiskTest> messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"LOADED"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/index.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| |
| done = false; |
| [messageHandler resetExpectedMessage:@"PASS: Registration was successful and service worker was activated"]; |
| |
| String thirdPartyIframeURL = URL(server.requestWithLocalhost("/thirdPartyIframeWithSW.html"_s).URL).string(); |
| String injectFrameScript = makeString("let frame = document.createElement('iframe'); frame.src = '"_s, thirdPartyIframeURL, "'; document.body.append(frame);"_s); |
| bool addedIframe = false; |
| [webView evaluateJavaScript:injectFrameScript.createNSString().get() completionHandler: [&] (id, NSError *error) { |
| EXPECT_TRUE(!error); |
| addedIframe = true; |
| }]; |
| |
| TestWebKitAPI::Util::run(&addedIframe); |
| TestWebKitAPI::Util::run(&done); |
| |
| [webView _close]; |
| webView = nullptr; |
| configuration = nullptr; |
| messageHandler = nullptr; |
| done = false; |
| |
| // Let the service worker process exit. |
| TestWebKitAPI::Util::runFor(1_s); |
| |
| configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| configuration.get().websiteDataStore = dataStore.get(); |
| |
| messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"LOADED"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/index.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| |
| done = false; |
| [messageHandler resetExpectedMessage:@"PASS: Registration already has an active worker"]; |
| |
| String thirdPartyIframeURL2 = URL(server.requestWithLocalhost("/thirdPartyIframeWithSW2.html"_s).URL).string(); |
| String injectFrameScript2 = makeString("let frame = document.createElement('iframe'); frame.src = '"_s, thirdPartyIframeURL2, "'; document.body.append(frame);"_s); |
| addedIframe = false; |
| [webView evaluateJavaScript:injectFrameScript2.createNSString().get() completionHandler: [&] (id, NSError *error) { |
| EXPECT_TRUE(!error); |
| addedIframe = true; |
| }]; |
| |
| TestWebKitAPI::Util::run(&addedIframe); |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [messageHandler resetExpectedMessage:@"Message from worker: Load succeeded"]; |
| |
| bool requestedFetch = false; |
| [webView evaluateJavaScript:@"frames[0].postMessage('do-fetch', '*');" completionHandler: [&] (id, NSError *error) { |
| EXPECT_TRUE(!error); |
| requestedFetch = true; |
| }]; |
| TestWebKitAPI::Util::run(&requestedFetch); |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, CacheStorageRestoreFromDisk) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainCacheStorageBytes } } |
| }); |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| auto messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"No cache storage data"]); |
| |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| webView = nullptr; |
| configuration = nullptr; |
| messageHandler = nullptr; |
| |
| configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"Some cache storage data: my cache"]); |
| |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, FetchAfterRestoreFromDisk) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| RetainPtr<SWMessageHandlerForFetchTest> messageHandler = adoptNS([[SWMessageHandlerForFetchTest alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainForFetchTestBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptHandlingFetchBytes } }, |
| }); |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| |
| webView = nullptr; |
| configuration = nullptr; |
| messageHandler = nullptr; |
| |
| done = false; |
| |
| configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| messageHandler = adoptNS([[SWMessageHandlerForFetchTest alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, InterceptFirstLoadAfterRestoreFromDisk) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| RetainPtr<SWMessageHandlerWithExpectedMessage> messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/main.html"_s, { mainForFirstLoadInterceptTestBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptInterceptingFirstLoadBytes } }, |
| }); |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| expectedMessage = "Service Worker activated"_s; |
| [webView loadRequest:server.request("/main.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| |
| webView = nullptr; |
| configuration = nullptr; |
| messageHandler = nullptr; |
| |
| done = false; |
| |
| configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| expectedMessage = "Intercepted by worker"_s; |
| [webView loadRequest:server.request("/main.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, MainThreadSWInterceptsLoad) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setViewDataStore(configuration.get(), ShouldRunServiceWorkersOnMainThread::Yes); |
| |
| RetainPtr<WKWebsiteDataStore> dataStore = [configuration websiteDataStore]; |
| |
| // Start with a clean slate data store |
| [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/main.html"_s, { mainForFirstLoadInterceptTestBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptInterceptingFirstLoadBytes } }, |
| }); |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| expectedMessage = "Service Worker activated"_s; |
| [webView loadRequest:server.request("/main.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| |
| webView = nullptr; |
| configuration = nullptr; |
| messageHandler = nullptr; |
| |
| done = false; |
| |
| configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [configuration setWebsiteDataStore:dataStore.get()]; |
| messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| expectedMessage = "Intercepted by worker"_s; |
| [webView loadRequest:server.request("/main.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, WaitForPolicyDelegate) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| RetainPtr<SWMessageHandlerWithExpectedMessage> messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/main.html"_s, { mainForFirstLoadInterceptTestBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptInterceptingFirstLoadBytes } }, |
| }); |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| // Register a service worker and activate it. |
| expectedMessage = "Service Worker activated"_s; |
| [webView loadRequest:server.request("/main.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| |
| webView = nullptr; |
| configuration = nullptr; |
| messageHandler = nullptr; |
| |
| done = false; |
| |
| configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| // Verify service worker is intercepting load. |
| expectedMessage = "Intercepted by worker"_s; |
| [webView loadRequest:server.request("/main.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| auto delegate = adoptNS([[TestSWAsyncNavigationDelegate alloc] init]); |
| [webView setNavigationDelegate:delegate.get()]; |
| [webView setUIDelegate:delegate.get()]; |
| |
| shouldAccept = true; |
| navigationFailed = false; |
| navigationComplete = false; |
| |
| // Verify service worker load goes well when policy delegate is ok. |
| [webView loadRequest:server.request("/main.html"_s)]; |
| TestWebKitAPI::Util::run(&navigationComplete); |
| |
| EXPECT_FALSE(navigationFailed); |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView setNavigationDelegate:delegate.get()]; |
| [webView setUIDelegate:delegate.get()]; |
| |
| shouldAccept = false; |
| navigationFailed = false; |
| navigationComplete = false; |
| |
| // Verify service worker load fails well when policy delegate is not ok. |
| [webView loadRequest:server.request("/main.html"_s)]; |
| TestWebKitAPI::Util::run(&navigationComplete); |
| |
| EXPECT_TRUE(navigationFailed); |
| } |
| |
| #if WK_HAVE_C_SPI |
| |
| void setConfigurationInjectedBundlePath(WKWebViewConfiguration* configuration) |
| { |
| WKRetainPtr<WKContextRef> context = adoptWK(TestWebKitAPI::Util::createContextForInjectedBundleTest("InternalsInjectedBundleTest")); |
| configuration.processPool = (WKProcessPool *)context.get(); |
| } |
| |
| @interface RegularPageMessageHandler : NSObject <WKScriptMessageHandler> |
| @end |
| |
| @implementation RegularPageMessageHandler |
| - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
| { |
| EXPECT_TRUE([[message body] isEqualToString:@"PASS"]); |
| done = true; |
| } |
| @end |
| |
| static constexpr auto regularPageWithConnectionBytes = R"SWRESOURCE( |
| <script> |
| window.webkit.messageHandlers.regularPage.postMessage("PASS"); |
| </script> |
| )SWRESOURCE"_s; |
| |
| static bool isSWProcessConnectionCreationTestSlow() |
| { |
| #if (!defined(NDEBUG) && PLATFORM(MAC) && __MAC_OS_X_VERSION_MIN_REQUIRED < 140000) |
| return true; |
| #else |
| return false; |
| #endif |
| } |
| |
| TEST(ServiceWorkers, SWProcessConnectionCreation) |
| { |
| if (isSWProcessConnectionCreationTestSlow()) |
| return; |
| |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| |
| done = false; |
| |
| [[configuration websiteDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [[configuration websiteDataStore] fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] completionHandler:^(NSArray<WKWebsiteDataRecord *> *websiteDataRecords) { |
| EXPECT_EQ(0u, [websiteDataRecords count]); |
| |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr<SWMessageHandler> messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| RetainPtr<RegularPageMessageHandler> regularPageMessageHandler = adoptNS([[RegularPageMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:regularPageMessageHandler.get() name:@"regularPage"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { regularPageWithConnectionBytes } }, |
| { "/second.html"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| { "/third.html"_s, { regularPageWithConnectionBytes } }, |
| { "/fourth.html"_s, { regularPageWithConnectionBytes } }, |
| }); |
| |
| RetainPtr<WKWebView> regularPageWebView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| RetainPtr<WKWebView> newRegularPageWebView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| // Test that a regular page does not trigger a service worker connection to network process if there is no registered service worker. |
| [regularPageWebView loadRequest:server.request("/first.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| // Test that a sw scheme page can register a service worker. |
| [webView loadRequest:server.request("/second.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| webView = nullptr; |
| |
| // Now that a service worker is registered, the regular page should have a service worker connection. |
| [regularPageWebView loadRequest:server.request("/third.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| regularPageWebView = nullptr; |
| |
| [newRegularPageWebView loadRequest:server.request("/fourth.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| newRegularPageWebView = nullptr; |
| |
| [[configuration websiteDataStore] fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] completionHandler:^(NSArray<WKWebsiteDataRecord *> *websiteDataRecords) { |
| EXPECT_EQ(1u, [websiteDataRecords count]); |
| EXPECT_WK_STREQ(websiteDataRecords[0].displayName, "127.0.0.1"); |
| |
| done = true; |
| }]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [[configuration websiteDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| static constexpr auto mainBytesWithScope = R"SWRESOURCE( |
| <script> |
| |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| navigator.serviceWorker.addEventListener("message", function(event) { |
| log("Message from worker: " + event.data); |
| }); |
| |
| try { |
| |
| navigator.serviceWorker.register('/sw.js', {scope: 'whateverscope'}).then(function(reg) { |
| reg.installing.postMessage("Hello from the web page"); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| |
| </script> |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorkers, ServiceWorkerProcessCreation) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| RetainPtr<WKProcessPool> originalProcessPool = configuration.get().processPool; |
| |
| done = false; |
| |
| [[configuration websiteDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [[configuration websiteDataStore] fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] completionHandler:^(NSArray<WKWebsiteDataRecord *> *websiteDataRecords) { |
| |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr<SWMessageHandler> messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| RetainPtr<RegularPageMessageHandler> regularPageMessageHandler = adoptNS([[RegularPageMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:regularPageMessageHandler.get() name:@"regularPage"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { mainBytesWithScope } }, |
| { "/second.html"_s, { regularPageWithConnectionBytes } }, |
| { "/third.html"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| // Load a page that registers a service worker. |
| [webView loadRequest:server.request("/first.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| webView = nullptr; |
| |
| // Now that a sw is registered, let's create a new configuration and try loading a regular page, there should be no service worker process created. |
| RetainPtr<WKWebViewConfiguration> newConfiguration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(newConfiguration.get()); |
| newConfiguration.get().websiteDataStore = [configuration websiteDataStore]; |
| |
| [[newConfiguration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| [[newConfiguration userContentController] addScriptMessageHandler:regularPageMessageHandler.get() name:@"regularPage"]; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:newConfiguration.get()]); |
| EXPECT_EQ(1u, webView.get().configuration.processPool._webProcessCountIgnoringPrewarmed); |
| [webView loadRequest:server.request("/second.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| // Make sure that loading the simple page did not start the service worker process. |
| EXPECT_EQ(1u, webView.get().configuration.processPool._serviceWorkerProcessCount); |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:newConfiguration.get()]); |
| EXPECT_EQ(2u, webView.get().configuration.processPool._webProcessCountIgnoringPrewarmed); |
| [webView loadRequest:server.request("/third.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_EQ(1u, webView.get().configuration.processPool._serviceWorkerProcessCount); |
| EXPECT_EQ(1u, originalProcessPool.get()._serviceWorkerProcessCount); |
| [[configuration websiteDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| static constexpr auto readCacheBytes = R"SWRESOURCE( |
| <script> |
| |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| window.caches.keys().then(keys => { |
| log(keys.length && keys[0] === "test" ? "PASS" : "FAIL"); |
| }, () => { |
| log("FAIL"); |
| }); |
| |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto writeCacheBytes = R"SWRESOURCE( |
| <script> |
| |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| window.caches.open("test").then(() => { |
| log("PASS"); |
| }, () => { |
| log("FAIL"); |
| }); |
| |
| </script> |
| )SWRESOURCE"_s; |
| |
| @interface SWMessageHandlerForCacheStorage : NSObject <WKScriptMessageHandler> |
| @end |
| |
| @implementation SWMessageHandlerForCacheStorage |
| - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
| { |
| EXPECT_WK_STREQ(@"PASS", [message body]); |
| done = true; |
| } |
| @end |
| |
| TEST(ServiceWorkers, CacheStorageInPrivateBrowsingMode) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerForCacheStorage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { writeCacheBytes } }, |
| { "/second.html"_s, { readCacheBytes } }, |
| }); |
| |
| configuration.get().websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView loadRequest:server.request("/first.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView loadRequest:server.request("/second.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| static constexpr auto serviceWorkerCacheAccessEphemeralSessionMainBytes = R"SWRESOURCE( |
| <script> |
| try { |
| navigator.serviceWorker.addEventListener("message", (event) => { |
| webkit.messageHandlers.sw.postMessage(event.data); |
| }); |
| |
| navigator.serviceWorker.register("serviceworker-private-browsing-worker.js", { scope : "my private backyard" }).then((registration) => { |
| activeWorker = registration.installing; |
| activeWorker.addEventListener('statechange', () => { |
| if (activeWorker.state === "activated") { |
| activeWorker.postMessage("TESTCACHE"); |
| } |
| }); |
| }); |
| } catch (e) { |
| webkit.messageHandlers.sw.postMessage("" + e); |
| } |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto serviceWorkerCacheAccessEphemeralSessionSWBytes = R"SWRESOURCE( |
| self.addEventListener("message", (event) => { |
| try { |
| self.caches.keys().then((keys) => { |
| event.source.postMessage(keys.length === 0 ? "PASS" : "FAIL: caches is not empty, got: " + JSON.stringify(keys)); |
| }); |
| } catch (e) { |
| event.source.postMessage("" + e); |
| } |
| }); |
| )SWRESOURCE"_s; |
| |
| // Opens a cache in the default session and checks that an ephemeral service worker |
| // does not have access to it. |
| TEST(ServiceWorkers, ServiceWorkerCacheAccessEphemeralSession) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto context = adoptWK(TestWebKitAPI::Util::createContextForInjectedBundleTest("InternalsInjectedBundleTest")); |
| [configuration setProcessPool:(WKProcessPool *)context.get()]; |
| |
| auto defaultPreferences = [configuration preferences]; |
| [defaultPreferences _setSecureContextChecksEnabled:NO]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { serviceWorkerCacheAccessEphemeralSessionMainBytes } }, |
| { "/serviceworker-private-browsing-worker.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, serviceWorkerCacheAccessEphemeralSessionSWBytes } }, |
| }); |
| |
| auto defaultWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [defaultWebView synchronouslyLoadHTMLString:@"foo" baseURL:server.request().URL]; |
| |
| bool openedCache = false; |
| [defaultWebView evaluateJavaScript:@"self.caches.open('test');" completionHandler: [&] (id innerText, NSError *error) { |
| openedCache = true; |
| }]; |
| TestWebKitAPI::Util::run(&openedCache); |
| |
| configuration.get().websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerForCacheStorage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| auto ephemeralWebView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [ephemeralWebView loadRequest:server.request()]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| static constexpr auto differentSessionsUseDifferentRegistrationsMainBytes = R"SWRESOURCE( |
| <script> |
| try { |
| navigator.serviceWorker.register("empty-worker.js", { scope : "/test" }).then((registration) => { |
| activeWorker = registration.installing; |
| activeWorker.addEventListener('statechange', () => { |
| if (activeWorker.state === "activated") |
| webkit.messageHandlers.sw.postMessage("PASS"); |
| }); |
| }); |
| } catch (e) { |
| webkit.messageHandlers.sw.postMessage("" + e); |
| } |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto defaultPageMainBytes = R"SWRESOURCE( |
| <script> |
| async function getResult() |
| { |
| var result = await internals.hasServiceWorkerRegistration("/test"); |
| window.webkit.messageHandlers.sw.postMessage(result ? "PASS" : "FAIL"); |
| } |
| getResult(); |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto privatePageMainBytes = R"SWRESOURCE( |
| <script> |
| async function getResult() |
| { |
| var result = await internals.hasServiceWorkerRegistration("/test"); |
| window.webkit.messageHandlers.sw.postMessage(result ? "FAIL" : "PASS"); |
| } |
| getResult(); |
| </script> |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorkers, DifferentSessionsUseDifferentRegistrations) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerForCacheStorage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { differentSessionsUseDifferentRegistrationsMainBytes } }, |
| { "/empty-worker.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, emptyString() } }, |
| { "/second.html"_s, { defaultPageMainBytes } }, |
| { "/third.html"_s, { privatePageMainBytes } }, |
| }); |
| |
| auto defaultWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [defaultWebView synchronouslyLoadRequest:server.request("/first.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [defaultWebView synchronouslyLoadRequest:server.request("/second.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| configuration.get().websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; |
| auto ephemeralWebView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [ephemeralWebView synchronouslyLoadRequest:server.request("/third.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| static constexpr auto regularPageGrabbingCacheStorageDirectory = R"SWRESOURCE( |
| <script> |
| async function getResult() |
| { |
| var result = await window.internals.cacheStorageEngineRepresentation(); |
| window.webkit.messageHandlers.sw.postMessage(result); |
| } |
| getResult(); |
| </script> |
| )SWRESOURCE"_s; |
| |
| @interface DirectoryPageMessageHandler : NSObject <WKScriptMessageHandler> |
| @end |
| |
| @implementation DirectoryPageMessageHandler |
| - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message |
| { |
| retrievedString = [message body]; |
| done = true; |
| } |
| @end |
| |
| TEST(ServiceWorkers, ServiceWorkerAndCacheStorageDefaultDirectories) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| |
| RetainPtr<DirectoryPageMessageHandler> directoryPageMessageHandler = adoptNS([[DirectoryPageMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:directoryPageMessageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| { "/second.html"_s, { regularPageGrabbingCacheStorageDirectory } }, |
| }); |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/first.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [configuration.get().websiteDataStore _storeServiceWorkerRegistrations:^{ |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/second.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| EXPECT_TRUE(retrievedString.contains("/Caches/com.apple.WebKit.TestWebKitAPI/WebKit/CacheStorage"_s)); |
| |
| [[configuration websiteDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, ServiceWorkerAndCacheStorageSpecificDirectories) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| auto dataStoreConfiguration = adoptNS([_WKWebsiteDataStoreConfiguration new]); |
| NSString* tempDirectory = @"/var/tmp"; |
| [dataStoreConfiguration _setServiceWorkerRegistrationDirectory:[NSURL fileURLWithPath:tempDirectory]]; |
| [dataStoreConfiguration _setCacheStorageDirectory:[NSURL fileURLWithPath:tempDirectory]]; |
| auto websiteDataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); |
| [configuration setWebsiteDataStore:websiteDataStore.get()]; |
| |
| RetainPtr<DirectoryPageMessageHandler> directoryPageMessageHandler = adoptNS([[DirectoryPageMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:directoryPageMessageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| { "/second.html"_s, { regularPageGrabbingCacheStorageDirectory } }, |
| }); |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/first.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| [websiteDataStore _storeServiceWorkerRegistrations:^{ |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/second.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| RetainPtr tempDirectoryInUse = FileSystem::realPath(tempDirectory).createNSString(); |
| String expectedString = [NSString stringWithFormat:@"\"path\": \"%@\"", tempDirectoryInUse.get()]; |
| EXPECT_TRUE(retrievedString.contains(expectedString)); |
| |
| [[configuration websiteDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| #endif // WK_HAVE_C_SPI |
| |
| TEST(ServiceWorkers, NonDefaultSessionID) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| NSURL *serviceWorkersPath = [NSURL fileURLWithPath:[@"~/Library/WebKit/com.apple.WebKit.TestWebKitAPI/CustomWebsiteData/ServiceWorkers/" stringByExpandingTildeInPath] isDirectory:YES]; |
| [[NSFileManager defaultManager] removeItemAtURL:serviceWorkersPath error:nil]; |
| EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:serviceWorkersPath.path]); |
| NSURL *idbPath = [NSURL fileURLWithPath:[@"~/Library/WebKit/com.apple.WebKit.TestWebKitAPI/CustomWebsiteData/IndexedDB/" stringByExpandingTildeInPath] isDirectory:YES]; |
| [[NSFileManager defaultManager] removeItemAtURL:idbPath error:nil]; |
| EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:idbPath.path]); |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| RetainPtr<_WKWebsiteDataStoreConfiguration> websiteDataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| websiteDataStoreConfiguration.get()._serviceWorkerRegistrationDirectory = serviceWorkersPath; |
| websiteDataStoreConfiguration.get()._indexedDBDatabaseDirectory = idbPath; |
| websiteDataStoreConfiguration.get().unifiedOriginStorageLevel = _WKUnifiedOriginStorageLevelBasic; |
| |
| configuration.get().websiteDataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]).get(); |
| |
| RetainPtr<SWMessageHandlerWithExpectedMessage> messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytesForSessionIDTest } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytesForSessionIDTest } }, |
| }); |
| |
| expectedMessage = "PASS: activation successful"_s; |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [webView _close]; |
| webView = nullptr; |
| |
| EXPECT_TRUE([[NSFileManager defaultManager] fileExistsAtPath:serviceWorkersPath.path]); |
| |
| [configuration.get().websiteDataStore fetchDataRecordsOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] completionHandler:^(NSArray<WKWebsiteDataRecord *> *websiteDataRecords) { |
| EXPECT_EQ(1u, [websiteDataRecords count]); |
| EXPECT_WK_STREQ(websiteDataRecords[0].displayName, "127.0.0.1"); |
| |
| done = true; |
| }]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| static bool waitUntilEvaluatesToTrue(const Function<bool()>& f, unsigned maxTimeout = 100) |
| { |
| unsigned timeout = 0; |
| do { |
| if (f()) |
| return true; |
| TestWebKitAPI::Util::runFor(0.1_s); |
| } while (++timeout < maxTimeout); |
| return false; |
| } |
| |
| TEST(ServiceWorkers, ProcessPerSite) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Normally, service workers get terminated several seconds after their clients are gone. |
| // Disable this delay for the purpose of testing. |
| auto dataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| [dataStoreConfiguration setServiceWorkerProcessTerminationDelayEnabled:NO]; |
| |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); |
| |
| // Start with a clean slate data store |
| [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| configuration.get().websiteDataStore = dataStore.get(); |
| |
| RetainPtr<SWMessageHandler> messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server1({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| TestWebKitAPI::HTTPServer server2({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| WKProcessPool *processPool = configuration.get().processPool; |
| |
| RetainPtr<WKWebView> webView1 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView1 loadRequest:server1.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_EQ(1U, processPool._serviceWorkerProcessCount); |
| |
| RetainPtr<WKWebView> webView2 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView2 loadRequest:server1.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_EQ(1U, processPool._serviceWorkerProcessCount); |
| |
| RetainPtr<WKWebView> webView3 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView3 loadRequest:server1.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_EQ(1U, processPool._serviceWorkerProcessCount); |
| |
| RetainPtr<WKWebView> webView4 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView4 loadRequest:server2.requestWithLocalhost()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_EQ(2U, processPool._serviceWorkerProcessCount); |
| |
| NSURLRequest *aboutBlankRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]; |
| [webView4 loadRequest:aboutBlankRequest]; |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return [processPool _serviceWorkerProcessCount] == 1; })); |
| EXPECT_EQ(1U, processPool._serviceWorkerProcessCount); |
| |
| [webView2 loadRequest:aboutBlankRequest]; |
| TestWebKitAPI::Util::spinRunLoop(10); |
| EXPECT_EQ(1U, processPool._serviceWorkerProcessCount); |
| |
| [webView1 loadRequest:aboutBlankRequest]; |
| [webView3 loadRequest:aboutBlankRequest]; |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return ![processPool _serviceWorkerProcessCount]; })); |
| EXPECT_EQ(0U, processPool._serviceWorkerProcessCount); |
| } |
| |
| TEST(ServiceWorkers, ParallelProcessLaunch) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server1({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| TestWebKitAPI::HTTPServer server2({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| auto *processPool = configuration.get().processPool; |
| |
| auto webView1 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| auto webView2 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView1 loadRequest:server1.request()]; |
| [webView2 loadRequest:server2.requestWithLocalhost()]; |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return [processPool _serviceWorkerProcessCount] == 2; })); |
| } |
| |
| static size_t launchServiceWorkerProcess(bool useSeparateServiceWorkerProcess, bool loadAboutBlankBeforePage) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, scriptBytes } } |
| }); |
| |
| auto *processPool = configuration.get().processPool; |
| [processPool _setUseSeparateServiceWorkerProcess: useSeparateServiceWorkerProcess]; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| auto navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]); |
| [navigationDelegate setDidFinishNavigation:^(WKWebView *, WKNavigation *) { |
| didFinishNavigationBoolean = true; |
| }]; |
| [webView setNavigationDelegate:navigationDelegate.get()]; |
| |
| if (loadAboutBlankBeforePage) { |
| didFinishNavigationBoolean = false; |
| [webView loadRequest: [NSURLRequest requestWithURL:[NSURL URLWithString:@"about:blank"]]]; |
| TestWebKitAPI::Util::run(&didFinishNavigationBoolean); |
| } |
| |
| [webView loadRequest:server.request()]; |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return [processPool _serviceWorkerProcessCount] == 1; })); |
| return webView.get().configuration.processPool._webProcessCountIgnoringPrewarmed; |
| } |
| |
| TEST(ServiceWorkers, OutOfProcessServiceWorker) |
| { |
| bool useSeparateServiceWorkerProcess = true; |
| bool firstLoadAboutBlank = true; |
| |
| EXPECT_EQ(1u, launchServiceWorkerProcess(!useSeparateServiceWorkerProcess, !firstLoadAboutBlank)); |
| } |
| |
| TEST(ServiceWorkers, InProcessServiceWorker) |
| { |
| bool useSeparateServiceWorkerProcess = true; |
| bool firstLoadAboutBlank = true; |
| |
| EXPECT_EQ(2u, launchServiceWorkerProcess(useSeparateServiceWorkerProcess, !firstLoadAboutBlank)); |
| } |
| |
| TEST(ServiceWorkers, LoadAboutBlankBeforeNavigatingThroughOutOfProcessServiceWorker) |
| { |
| bool useSeparateServiceWorkerProcess = true; |
| bool firstLoadAboutBlank = true; |
| |
| EXPECT_EQ(1u, launchServiceWorkerProcess(!useSeparateServiceWorkerProcess, firstLoadAboutBlank)); |
| } |
| |
| TEST(ServiceWorkers, LoadAboutBlankBeforeNavigatingThroughInProcessServiceWorker) |
| { |
| bool useSeparateServiceWorkerProcess = true; |
| bool firstLoadAboutBlank = true; |
| |
| EXPECT_EQ(2u, launchServiceWorkerProcess(useSeparateServiceWorkerProcess, firstLoadAboutBlank)); |
| } |
| |
| |
| static constexpr auto mainSharedWorkerBytes = R"SWRESOURCE( |
| <script> |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| var sharedWorker; |
| |
| try { |
| var sharedWorker = new SharedWorker("sharedWorker.js"); |
| sharedWorker.port.start(); |
| sharedWorker.port.postMessage("Hello from the web page"); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| |
| sharedWorker.port.onmessage = e => { |
| log("Message from worker: " + e.data); |
| }; |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto sharedWorkerScriptWithEvalBytes = R"SWRESOURCE( |
| onconnect = e => { |
| const port = e.ports[0]; |
| port.onmessage = event => { |
| if (event.data == "Hello from the web page") { |
| port.postMessage("SharedWorker received: " + event.data); |
| return; |
| } |
| port.postMessage("Evaluation result: " + eval(event.data)); |
| }; |
| port.start(); |
| }; |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorkers, LockdownModeInSharedWorkerProcess) |
| { |
| // Turn on lockdown mode globally. |
| [WKProcessPool _setCaptivePortalModeEnabledGloballyForTesting:YES]; |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| TestWebKitAPI::Util::spinRunLoop(); |
| |
| // Start with a clean slate data store. |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| RetainPtr messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"Message from worker: SharedWorker received: Hello from the web page"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainSharedWorkerBytes } }, |
| { "/sharedWorker.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, sharedWorkerScriptWithEvalBytes } } |
| }); |
| |
| RetainPtr processPool = retainPtr(configuration.get().processPool); |
| |
| // Make sure that the service worker launches in its own process. |
| [processPool _setUseSeparateServiceWorkerProcess:YES]; |
| |
| RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| RetainPtr navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]); |
| [navigationDelegate setDidFinishNavigation:^(WKWebView *, WKNavigation *) { |
| didFinishNavigationBoolean = true; |
| }]; |
| [webView setNavigationDelegate:navigationDelegate.get()]; |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return [processPool _webProcessCount] == 2; })); |
| |
| // Check that JIT is disabled in the service worker process. |
| done = false; |
| [processPool _isJITDisabledInAllRemoteWorkerProcesses:^(BOOL isJITDisabled) { |
| EXPECT_TRUE(isJITDisabled); |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| |
| auto runJSCheck = [&](const String& jsToEvalInWorker) { |
| bool finishedRunningScript = false; |
| done = false; |
| auto js = makeString("sharedWorker.port.postMessage('"_s, jsToEvalInWorker,"');"_s); |
| [webView evaluateJavaScript:js.createNSString().get() completionHandler:[&] (id result, NSError *error) { |
| EXPECT_NULL(error); |
| finishedRunningScript = true; |
| }]; |
| TestWebKitAPI::Util::run(&finishedRunningScript); |
| TestWebKitAPI::Util::run(&done); |
| }; |
| |
| [messageHandler resetExpectedMessage:@"Message from worker: Evaluation result: true"]; |
| runJSCheck("!!self.URL"_s); |
| |
| // Check individual settings that are meant to be disabled in lockdown mode. |
| [messageHandler resetExpectedMessage:@"Message from worker: Evaluation result: false"]; |
| runJSCheck("!!self.WebGL2RenderingContext"_s); |
| runJSCheck("!!self.FileSystemHandle"_s); // File System Access. |
| #if ENABLE(NOTIFICATIONS) |
| runJSCheck("!!self.Notification"_s); // Notification API. |
| #endif |
| runJSCheck("!!self.Cache"_s); // Cache API. |
| runJSCheck("!!self.CacheStorage"_s); // Cache API. |
| runJSCheck("!!self.FileReader"_s); // FileReader API. |
| runJSCheck("!!self.FileSystemFileHandle"_s); // File System Access API. |
| runJSCheck("!!self.PushManager"_s); // Push API. |
| runJSCheck("!!self.PushSubscription"_s); // Push API. |
| runJSCheck("!!self.PushSubscriptionOptions"_s); // Push API. |
| runJSCheck("!!self.LockManager"_s); // WebLockManager API. |
| } |
| |
| enum class UseSeparateServiceWorkerProcess : bool { No, Yes }; |
| void testSuspendServiceWorkerProcessBasedOnClientProcesses(UseSeparateServiceWorkerProcess useSeparateServiceWorkerProcess) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| auto *processPool = configuration.get().processPool; |
| [processPool _setUseSeparateServiceWorkerProcess:(useSeparateServiceWorkerProcess == UseSeparateServiceWorkerProcess::Yes)]; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request()]; |
| [webView _test_waitForDidFinishNavigation]; |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return [processPool _serviceWorkerProcessCount] == 1; })); |
| |
| auto webView2 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView2 loadRequest:server.request()]; |
| [webView2 _test_waitForDidFinishNavigation]; |
| |
| auto webViewToUpdate = useSeparateServiceWorkerProcess == UseSeparateServiceWorkerProcess::Yes ? webView : webView2; |
| |
| if (useSeparateServiceWorkerProcess == UseSeparateServiceWorkerProcess::Yes) |
| [webView2 _setThrottleStateForTesting:0]; |
| |
| [webViewToUpdate _setThrottleStateForTesting:1]; |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { |
| return ![webViewToUpdate _hasServiceWorkerForegroundActivityForTesting] && [webViewToUpdate _hasServiceWorkerBackgroundActivityForTesting]; |
| })); |
| |
| [webViewToUpdate _setThrottleStateForTesting:2]; |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { |
| return [webViewToUpdate _hasServiceWorkerForegroundActivityForTesting] && ![webViewToUpdate _hasServiceWorkerBackgroundActivityForTesting]; |
| })); |
| |
| [webViewToUpdate _setThrottleStateForTesting:0]; |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { |
| return ![webViewToUpdate _hasServiceWorkerForegroundActivityForTesting] && ![webViewToUpdate _hasServiceWorkerBackgroundActivityForTesting]; |
| })); |
| |
| [webView _close]; |
| webView = nullptr; |
| |
| // The service worker process should take activity based on webView2 process. |
| [webView2 _setThrottleStateForTesting: 1]; |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { |
| [webView2 _setThrottleStateForTesting:1]; |
| return ![webView2 _hasServiceWorkerForegroundActivityForTesting] && [webView2 _hasServiceWorkerBackgroundActivityForTesting]; |
| })); |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { |
| [webView2 _setThrottleStateForTesting:2]; |
| return [webView2 _hasServiceWorkerForegroundActivityForTesting] && ![webView2 _hasServiceWorkerBackgroundActivityForTesting]; |
| })); |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { |
| [webView2 _setThrottleStateForTesting:0]; |
| return ![webView2 _hasServiceWorkerForegroundActivityForTesting] && ![webView2 _hasServiceWorkerBackgroundActivityForTesting]; |
| })); |
| } |
| |
| TEST(ServiceWorkers, SuspendServiceWorkerProcessBasedOnClientProcessesWithSeparateServiceWorkerProcess) |
| { |
| testSuspendServiceWorkerProcessBasedOnClientProcesses(UseSeparateServiceWorkerProcess::Yes); |
| } |
| |
| TEST(ServiceWorkers, SuspendServiceWorkerProcessBasedOnClientProcessesWithoutSeparateServiceWorkerProcess) |
| { |
| testSuspendServiceWorkerProcessBasedOnClientProcesses(UseSeparateServiceWorkerProcess::No); |
| } |
| |
| static bool isPIDSuspended(pid_t pid) |
| { |
| mach_port_t task = MACH_PORT_NULL; |
| if (task_name_for_pid(mach_task_self(), pid, &task) != KERN_SUCCESS) |
| return false; |
| |
| auto scope = makeScopeExit([task]() { |
| mach_port_deallocate(mach_task_self(), task); |
| }); |
| |
| mach_task_basic_info_data_t basicInfo; |
| mach_msg_type_number_t basicInfoCount = MACH_TASK_BASIC_INFO_COUNT; |
| if (task_info(task, MACH_TASK_BASIC_INFO, (task_info_t)&basicInfo, &basicInfoCount) != KERN_SUCCESS) |
| return false; |
| |
| return basicInfo.suspend_count; |
| } |
| |
| TEST(ServiceWorkers, SuspendAndTerminateWorker) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| RetainPtr dataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| [dataStoreConfiguration setServiceWorkerProcessTerminationDelayEnabled:NO]; |
| RetainPtr dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); |
| |
| // Start with a clean slate data store |
| [dataStore removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| if ([[configuration preferences] inactiveSchedulingPolicy] == WKInactiveSchedulingPolicyNone) |
| return; |
| |
| [[configuration preferences] setInactiveSchedulingPolicy:WKInactiveSchedulingPolicySuspend]; |
| |
| RetainPtr messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| auto *processPool = configuration.get().processPool; |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| expectedMessage = "Message from worker: ServiceWorker received: Hello from the web page"_s; |
| [webView loadRequest:server.request()]; |
| // Wait until ServiceWorker is launched. |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return [processPool _serviceWorkerProcessCount] == 1; })); |
| |
| [webView _setThrottleStateForTesting:0]; |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { |
| return ![webView _hasServiceWorkerForegroundActivityForTesting] && ![webView _hasServiceWorkerBackgroundActivityForTesting]; |
| })); |
| |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return [webView _webProcessState] == _WKWebProcessStateSuspended; })); |
| |
| // Due to the process assertion cache, the process can actually run for a little while after it |
| // enters the suspended state. Wait until the OS tells us the process is actually suspended. |
| pid_t pidBeforeTerminatingServiceWorker = [webView _webProcessIdentifier]; |
| EXPECT_TRUE(waitUntilEvaluatesToTrue([&] { return isPIDSuspended(pidBeforeTerminatingServiceWorker); })); |
| |
| // Process with service worker and page is suspended, let's terminate the service worker. |
| [dataStore removeDataOfTypes:[NSSet setWithObject:WKWebsiteDataTypeServiceWorkerRegistrations] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| // ServiceWorker should be terminated after 1s (WebCore::SWServerWorker::startTermination::terminationDelayForTesting), |
| // so we wait for 2s. |
| bool serviceWorkersAllTerminated = waitUntilEvaluatesToTrue([&]() { |
| __block bool done; |
| __block NSUInteger serviceWorkerCount; |
| |
| [dataStore _runningOrTerminatingServiceWorkerCountForTesting:^(NSUInteger count) { |
| serviceWorkerCount = count; |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| |
| return !serviceWorkerCount; |
| }, 20); |
| EXPECT_TRUE(serviceWorkersAllTerminated); |
| |
| // Let's verify the WKWebView did not crash and has the same PID as before. |
| expectedMessage = "OK"_s; |
| [webView evaluateJavaScript:@"log('OK')" completionHandler: nil]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| pid_t pidAfterTerminatingServiceWorker = [webView _webProcessIdentifier]; |
| EXPECT_EQ(pidBeforeTerminatingServiceWorker, pidAfterTerminatingServiceWorker); |
| } |
| |
| TEST(ServiceWorkers, ThrottleCrash) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, scriptBytes } }, |
| }); |
| |
| auto navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]); |
| [navigationDelegate setDidFinishNavigation:^(WKWebView *, WKNavigation *) { |
| didFinishNavigationBoolean = true; |
| }]; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| #if PLATFORM(MAC) |
| [[configuration preferences] _setAppNapEnabled:YES]; |
| #endif |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| auto *processPool = configuration.get().processPool; |
| |
| auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get() addToWindow: YES]); |
| [webView1 setNavigationDelegate:navigationDelegate.get()]; |
| |
| // Let's make it so that webView1 be app nappable after loading is completed. |
| [webView1.get().window resignKeyWindow]; |
| #if PLATFORM(MAC) |
| [webView1.get().window orderOut:nil]; |
| #endif |
| |
| [webView1 loadRequest:server.request()]; |
| |
| didFinishNavigationBoolean = false; |
| TestWebKitAPI::Util::run(&didFinishNavigationBoolean); |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto webView2Configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| #if PLATFORM(MAC) |
| [[webView2Configuration preferences] _setAppNapEnabled:NO]; |
| #endif |
| [webView2Configuration setProcessPool:processPool]; |
| [[webView2Configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| webView2Configuration.get()._relatedWebView = webView1.get(); |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| auto webView2 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:webView2Configuration.get()]); |
| [webView2 setNavigationDelegate:navigationDelegate.get()]; |
| |
| [webView2 loadRequest:server.request()]; |
| |
| didFinishNavigationBoolean = false; |
| TestWebKitAPI::Util::run(&didFinishNavigationBoolean); |
| } |
| |
| TEST(ServiceWorkers, LoadData) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| RetainPtr<SWMessageHandler> messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| auto delegate = adoptNS([[TestSWAsyncNavigationDelegate alloc] init]); |
| [webView setNavigationDelegate:delegate.get()]; |
| [webView setUIDelegate:delegate.get()]; |
| |
| done = false; |
| |
| // Normal load to get SW registered. |
| [webView loadRequest:server.request()]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| // Now try a data load. |
| NSData *data = [NSData dataWithBytes:mainBytes length:strlen(mainBytes)]; |
| [webView loadData:data MIMEType:@"text/html" characterEncodingName:@"UTF-8" baseURL:server.request().URL]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| TEST(ServiceWorkers, RestoreFromDiskNonDefaultStore) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| NSURL *swDBPath = [NSURL fileURLWithPath:[@"~/Library/WebKit/com.apple.WebKit.TestWebKitAPI/CustomWebsiteData/ServiceWorkers/" stringByExpandingTildeInPath]]; |
| [[NSFileManager defaultManager] removeItemAtURL:swDBPath error:nil]; |
| EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:swDBPath.path]); |
| [[NSFileManager defaultManager] createDirectoryAtURL:swDBPath withIntermediateDirectories:YES attributes:nil error:nil]; |
| EXPECT_TRUE([[NSFileManager defaultManager] fileExistsAtPath:swDBPath.path]); |
| |
| RetainPtr<WKProcessPool> protectedProcessPool; |
| RetainPtr<WKWebsiteDataStore> protectedWebsiteDataStore; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { mainRegisteringWorkerBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| { "/second.html"_s, { mainRegisteringAlreadyExistingWorkerBytes } }, |
| }); |
| |
| @autoreleasepool { |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto websiteDataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| websiteDataStoreConfiguration.get()._serviceWorkerRegistrationDirectory = swDBPath; |
| auto nonDefaultDataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]); |
| configuration.get().websiteDataStore = nonDefaultDataStore.get(); |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"PASS: Registration was successful and service worker was activated"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| protectedProcessPool = webView.get().configuration.processPool; |
| protectedWebsiteDataStore = webView.get().configuration.websiteDataStore; |
| |
| [webView loadRequest:server.request("/first.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [webView.get().configuration.processPool _terminateServiceWorkers]; |
| } |
| |
| // Make us more likely to lose any races with service worker initialization. |
| TestWebKitAPI::Util::spinRunLoop(10); |
| usleep(10000); |
| TestWebKitAPI::Util::spinRunLoop(10); |
| |
| @autoreleasepool { |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| configuration.get().websiteDataStore = protectedWebsiteDataStore.get(); |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerForRestoreFromDiskTest alloc] initWithExpectedMessage:@"PASS: Registration already has an active worker"]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView loadRequest:server.request("/second.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| } |
| |
| TEST(ServiceWorkers, SuspendNetworkProcess) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| auto websiteDataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| websiteDataStoreConfiguration.get().unifiedOriginStorageLevel = _WKUnifiedOriginStorageLevelBasic; |
| auto websiteDataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]); |
| [websiteDataStore.get() removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| RetainPtr<WKWebViewConfiguration> configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [configuration setWebsiteDataStore:websiteDataStore.get()]; |
| RetainPtr<SWMessageHandler> messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/first.html"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| { "/second.html"_s, { mainBytes } }, |
| }); |
| |
| RetainPtr<WKWebView> webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| auto delegate = adoptNS([[TestSWAsyncNavigationDelegate alloc] init]); |
| [webView setNavigationDelegate:delegate.get()]; |
| [webView setUIDelegate:delegate.get()]; |
| |
| done = false; |
| |
| // Normal load to get SW registered. |
| [webView loadRequest:server.request("/first.html"_s)]; |
| |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto store = [configuration websiteDataStore]; |
| auto path = store._configuration._serviceWorkerRegistrationDirectory.path; |
| |
| NSURL* directory = [NSURL fileURLWithPath:path isDirectory:YES]; |
| NSURL *swDBPath = [directory URLByAppendingPathComponent:serviceWorkerRegistrationFilename]; |
| |
| unsigned timeout = 0; |
| while (![[NSFileManager defaultManager] fileExistsAtPath:swDBPath.path] && ++timeout < 100) |
| TestWebKitAPI::Util::runFor(0.1_s); |
| |
| EXPECT_TRUE([[NSFileManager defaultManager] fileExistsAtPath:swDBPath.path]); |
| |
| [webView.get().configuration.websiteDataStore _sendNetworkProcessWillSuspendImminently]; |
| |
| EXPECT_TRUE([[NSFileManager defaultManager] fileExistsAtPath:swDBPath.path]); |
| |
| [webView.get().configuration.websiteDataStore _sendNetworkProcessDidResume]; |
| |
| [webView loadRequest:server.request("/second.html"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_TRUE([[NSFileManager defaultManager] fileExistsAtPath:swDBPath.path]); |
| } |
| |
| TEST(WebKit, ServiceWorkerDatabaseWithRecordsTableButUnexpectedSchema) |
| { |
| // Copy the baked database files to the database directory |
| NSURL *url1 = [NSBundle.test_resourcesBundle URLForResource:@"BadServiceWorkerRegistrations-4" withExtension:@"sqlite3"]; |
| |
| NSURL *swPath = [NSURL fileURLWithPath:[@"~/Library/Caches/com.apple.WebKit.TestWebKitAPI/WebKit/ServiceWorkers/" stringByExpandingTildeInPath]]; |
| [[NSFileManager defaultManager] removeItemAtURL:swPath error:nil]; |
| EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:swPath.path]); |
| |
| [[NSFileManager defaultManager] createDirectoryAtURL:swPath withIntermediateDirectories:YES attributes:nil error:nil]; |
| [[NSFileManager defaultManager] copyItemAtURL:url1 toURL:[swPath URLByAppendingPathComponent:serviceWorkerRegistrationFilename] error:nil]; |
| |
| auto websiteDataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| websiteDataStoreConfiguration.get()._serviceWorkerRegistrationDirectory = swPath; |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]); |
| |
| // Fetch SW records |
| auto websiteDataTypes = adoptNS([[NSSet alloc] initWithArray:@[WKWebsiteDataTypeServiceWorkerRegistrations]]); |
| static bool readyToContinue; |
| [dataStore fetchDataRecordsOfTypes:websiteDataTypes.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) { |
| EXPECT_EQ(0U, dataRecords.count); |
| readyToContinue = true; |
| }]; |
| TestWebKitAPI::Util::run(&readyToContinue); |
| readyToContinue = false; |
| } |
| |
| TEST(ServiceWorkers, V2DatabaseUpgrade) |
| { |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| NSURL *swPath = [NSURL fileURLWithPath:[@"~/Library/Caches/com.apple.WebKit.TestWebKitAPI/WebKit/ServiceWorkers/" stringByExpandingTildeInPath]]; |
| [[NSFileManager defaultManager] removeItemAtURL:swPath error:nil]; |
| EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:swPath.path]); |
| |
| NSURL *scriptDirectoyPath = [NSURL fileURLWithPath:[@"~/Library/Caches/com.apple.WebKit.TestWebKitAPI/WebKit/ServiceWorkers/Scripts/V1/xPAvXL4WjRQdGV0mpfwak2b5_1GqLUx4U9UZRpYn2Jw/wc4tzUMAvirBHXPD6rEkc5sG5oyRXMuxJl04TscgvKI/" stringByExpandingTildeInPath]]; |
| [[NSFileManager defaultManager] createDirectoryAtURL:scriptDirectoyPath withIntermediateDirectories:YES attributes:nil error:nil]; |
| |
| // Copy the baked database files to the database directory |
| NSURL *registrationDatabaseResource = [NSBundle.test_resourcesBundle URLForResource:@"ServiceWorkerRegistrationsVersion1" withExtension:@"sqlite3"]; |
| [[NSFileManager defaultManager] copyItemAtURL:registrationDatabaseResource toURL:[swPath URLByAppendingPathComponent:serviceWorkerRegistrationFilename] error:nil]; |
| |
| // Copy the salt file to have fixed hashed script filenames. |
| NSURL *saltResource = [NSBundle.test_resourcesBundle URLForResource:@"ServiceWorkerRegistrationsVersion1Salt" withExtension:@"bin"]; |
| NSURL *saltPath = [NSURL fileURLWithPath:[@"~/Library/Caches/com.apple.WebKit.TestWebKitAPI/WebKit/ServiceWorkers/Scripts/V1/salt" stringByExpandingTildeInPath]]; |
| [[NSFileManager defaultManager] copyItemAtURL:saltResource toURL:saltPath error:nil]; |
| |
| // Copy a fake script so that registration import is successful. |
| NSURL *scriptPath = [NSURL fileURLWithPath:[@"~/Library/Caches/com.apple.WebKit.TestWebKitAPI/WebKit/ServiceWorkers/Scripts/V1/xPAvXL4WjRQdGV0mpfwak2b5_1GqLUx4U9UZRpYn2Jw/wc4tzUMAvirBHXPD6rEkc5sG5oyRXMuxJl04TscgvKI/bNUYH4V9T2WYLnx37jHP2cE6FjNkL078xwiNeE465Bs" stringByExpandingTildeInPath]]; |
| [[NSFileManager defaultManager] copyItemAtURL:saltResource toURL:scriptPath error:nil]; |
| |
| auto websiteDataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| websiteDataStoreConfiguration.get()._serviceWorkerRegistrationDirectory = swPath; |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]); |
| |
| // Fetch SW records |
| auto websiteDataTypes = adoptNS([[NSSet alloc] initWithArray:@[WKWebsiteDataTypeServiceWorkerRegistrations]]); |
| static bool readyToContinue; |
| [dataStore fetchDataRecordsOfTypes:websiteDataTypes.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) { |
| EXPECT_EQ(1U, dataRecords.count); |
| readyToContinue = true; |
| }]; |
| TestWebKitAPI::Util::run(&readyToContinue); |
| readyToContinue = false; |
| } |
| |
| TEST(ServiceWorkers, ProcessPerSession) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server1({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| WKProcessPool *processPool = configuration.get().processPool; |
| |
| auto webView1 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView1 loadRequest:server1.request()]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_EQ(1U, processPool._serviceWorkerProcessCount); |
| |
| configuration.get().websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; |
| |
| TestWebKitAPI::HTTPServer server2({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| auto webView2 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView2 loadRequest:server2.requestWithLocalhost()]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| EXPECT_EQ(2U, processPool._serviceWorkerProcessCount); |
| } |
| |
| static constexpr auto contentRuleListWorkerScript = |
| "self.addEventListener('message', (event) => {" |
| " fetch('blockedsubresource').then(() => {" |
| " event.source.postMessage('FAIL - should have blocked first request');" |
| " }).catch(() => {" |
| " fetch('allowedsubresource').then(() => {" |
| " event.source.postMessage('PASS - blocked first request, allowed second');" |
| " }).catch(() => {" |
| " event.source.postMessage('FAIL - should have allowed second request');" |
| " });" |
| " });" |
| "});"_s; |
| |
| TEST(ServiceWorkers, ContentRuleList) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| __block bool doneCompiling = false; |
| __block RetainPtr<WKContentRuleList> contentRuleList; |
| [[WKContentRuleListStore defaultStore] compileContentRuleListForIdentifier:@"ServiceWorkerRuleList" encodedContentRuleList:@"[{\"action\":{\"type\":\"block\"},\"trigger\":{\"url-filter\":\"blockedsubresource\"}}]" completionHandler:^(WKContentRuleList *list, NSError *error) { |
| EXPECT_NOT_NULL(list); |
| EXPECT_NULL(error); |
| contentRuleList = list; |
| doneCompiling = true; |
| }]; |
| TestWebKitAPI::Util::run(&doneCompiling); |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| [[configuration userContentController] addContentRuleList:contentRuleList.get()]; |
| |
| using namespace TestWebKitAPI; |
| HTTPServer server([] (Connection connection) { |
| connection.receiveHTTPRequest([=](Vector<char>&&) { |
| connection.send(HTTPResponse({ { "Content-Type"_s, "text/html"_s } }, mainBytes).serialize(), [=] { |
| connection.receiveHTTPRequest([=](Vector<char>&&) { |
| connection.send(HTTPResponse({ { "Content-Type"_s, "application/javascript"_s } }, contentRuleListWorkerScript).serialize(), [=] { |
| connection.receiveHTTPRequest([=](Vector<char>&& lastRequest) { |
| EXPECT_TRUE(contains(lastRequest.span(), "allowedsubresource"_span)); |
| connection.send(HTTPResponse("successful fetch"_s).serialize()); |
| }); |
| }); |
| }); |
| }); |
| }); |
| }); |
| |
| expectedMessage = @"Message from worker: PASS - blocked first request, allowed second"; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://127.0.0.1:%d/", server.port()]]]]; |
| TestWebKitAPI::Util::run(&done); |
| |
| __block bool doneRemoving = false; |
| [[WKContentRuleListStore defaultStore] removeContentRuleListForIdentifier:@"ServiceWorkerRuleList" completionHandler:^(NSError *error) { |
| EXPECT_NULL(error); |
| doneRemoving = true; |
| }]; |
| TestWebKitAPI::Util::run(&doneRemoving); |
| } |
| |
| static bool isTestServerTrust(SecTrustRef trust) |
| { |
| if (!trust) |
| return false; |
| if (SecTrustGetCertificateCount(trust) != 1) |
| return false; |
| |
| auto chain = adoptCF(SecTrustCopyCertificateChain(trust)); |
| auto certificate = checked_cf_cast<SecCertificateRef>(CFArrayGetValueAtIndex(chain.get(), 0)); |
| if (![bridge_cast(adoptCF(SecCertificateCopySubjectSummary(certificate)).get()) isEqualToString:@"Me"]) |
| return false; |
| |
| return true; |
| } |
| |
| enum class ResponseType { Synthetic, Cached, Fetched }; |
| static void runTest(ResponseType responseType) |
| { |
| using namespace TestWebKitAPI; |
| |
| __block bool removedAnyExistingData = false; |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| removedAnyExistingData = true; |
| }]; |
| TestWebKitAPI::Util::run(&removedAnyExistingData); |
| |
| static constexpr auto main = |
| "<script>" |
| "try {" |
| " navigator.serviceWorker.register('/sw.js').then(function(reg) {" |
| " if (reg.active) {" |
| " alert('worker unexpectedly already active');" |
| " return;" |
| " }" |
| " worker = reg.installing;" |
| " worker.addEventListener('statechange', function() {" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " }).catch(function(error) {" |
| " alert('Registration failed with: ' + error);" |
| " });" |
| "} catch(e) {" |
| " alert('Exception: ' + e);" |
| "}" |
| "</script>"_s; |
| |
| ASCIILiteral js = ""_s; |
| const char* expectedAlert = nullptr; |
| size_t expectedServerRequests1 = 0; |
| size_t expectedServerRequests2 = 0; |
| |
| switch (responseType) { |
| case ResponseType::Synthetic: |
| js = "self.addEventListener('fetch', (event) => { event.respondWith(new Response(new Blob(['<script>alert(\"synthetic response\")</script>'], {type: 'text/html'}))); })"_s; |
| expectedAlert = "synthetic response"; |
| expectedServerRequests1 = 2; |
| expectedServerRequests2 = 2; |
| break; |
| case ResponseType::Cached: |
| js = "self.addEventListener('install', (event) => { event.waitUntil( caches.open('v1').then((cache) => { return cache.addAll(['/cached.html']); }) ); });" |
| "self.addEventListener('fetch', (event) => { event.respondWith(caches.match('/cached.html')) });"_s; |
| expectedAlert = "loaded from cache"; |
| expectedServerRequests1 = 3; |
| expectedServerRequests2 = 3; |
| break; |
| case ResponseType::Fetched: |
| js = "self.addEventListener('fetch', (event) => { event.respondWith(fetch('/fetched.html')) });"_s; |
| expectedAlert = "fetched from server"; |
| expectedServerRequests1 = 2; |
| expectedServerRequests2 = 3; |
| break; |
| } |
| |
| HTTPServer server({ |
| { "/"_s, { main } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, js } }, |
| { "/cached.html"_s, { "<script>alert('loaded from cache')</script>"_s } }, |
| { "/fetched.html"_s, { "<script>alert('fetched from server')</script>"_s } }, |
| }, HTTPServer::Protocol::Https); |
| |
| auto webView = adoptNS([WKWebView new]); |
| |
| auto delegate = adoptNS([TestNavigationDelegate new]); |
| [delegate setDidReceiveAuthenticationChallenge:^(WKWebView *, NSURLAuthenticationChallenge *challenge, void (^callback)(NSURLSessionAuthChallengeDisposition, NSURLCredential *)) { |
| EXPECT_WK_STREQ(challenge.protectionSpace.authenticationMethod, NSURLAuthenticationMethodServerTrust); |
| callback(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); |
| }]; |
| webView.get().navigationDelegate = delegate.get(); |
| |
| [webView loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "successfully registered"); |
| |
| EXPECT_EQ(server.totalRequests(), expectedServerRequests1); |
| EXPECT_TRUE(isTestServerTrust(webView.get().serverTrust)); |
| if (responseType != ResponseType::Fetched) |
| server.cancel(); |
| |
| [webView reload]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], expectedAlert); |
| EXPECT_EQ(server.totalRequests(), expectedServerRequests2); |
| EXPECT_TRUE(isTestServerTrust(webView.get().serverTrust)); |
| } |
| |
| TEST(ServiceWorkers, ServerTrust) |
| { |
| runTest(ResponseType::Synthetic); |
| runTest(ResponseType::Cached); |
| runTest(ResponseType::Fetched); |
| } |
| |
| TEST(ServiceWorkers, ChangeOfServerCertificate) |
| { |
| __block bool removedAnyExistingData = false; |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| removedAnyExistingData = true; |
| }]; |
| TestWebKitAPI::Util::run(&removedAnyExistingData); |
| |
| static constexpr auto main = |
| "<script>" |
| "async function test() {" |
| " try {" |
| " const registration = await navigator.serviceWorker.register('/sw.js');" |
| " if (registration.active) {" |
| " registration.onupdatefound = () => alert('new worker');" |
| " setTimeout(() => alert('no update found'), 5000);" |
| " return;" |
| " }" |
| " worker = registration.installing;" |
| " worker.addEventListener('statechange', () => {" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " } catch(e) {" |
| " alert('Exception: ' + e);" |
| " }" |
| "}" |
| "window.onload = test;" |
| "</script>"_s; |
| static constexpr auto js = ""_s; |
| |
| auto delegate = adoptNS([TestNavigationDelegate new]); |
| [delegate setDidReceiveAuthenticationChallenge:^(WKWebView *, NSURLAuthenticationChallenge *challenge, void (^callback)(NSURLSessionAuthChallengeDisposition, NSURLCredential *)) { |
| EXPECT_WK_STREQ(challenge.protectionSpace.authenticationMethod, NSURLAuthenticationMethodServerTrust); |
| callback(NSURLSessionAuthChallengeUseCredential, [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust]); |
| }]; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| auto webView1 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| auto webView2 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| webView1.get().navigationDelegate = delegate.get(); |
| webView2.get().navigationDelegate = delegate.get(); |
| |
| uint16_t serverPort; |
| |
| // Load webView1 with a first server. |
| { |
| TestWebKitAPI::HTTPServer server1({ |
| { "/"_s, { main } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, js } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Https, nullptr, testIdentity().get()); |
| serverPort = server1.port(); |
| |
| [webView1 loadRequest:server1.request()]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "successfully registered"); |
| |
| server1.cancel(); |
| } |
| |
| // Load webView2 with a second server on same port with a different certificate |
| // This should trigger installing a new worker. |
| { |
| TestWebKitAPI::HTTPServer server2({ |
| { "/"_s, { main } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, js } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Https, nullptr, testIdentity2().get(), serverPort); |
| |
| [webView2 loadRequest:server2.request()]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "new worker"); |
| } |
| } |
| |
| TEST(ServiceWorkers, ClearDOMCacheAlsoIncludesServiceWorkerRegistrations) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView loadRequest:server.request()]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| // Fetch SW records |
| auto websiteDataTypes = adoptNS([[NSSet alloc] initWithArray:@[WKWebsiteDataTypeServiceWorkerRegistrations]]); |
| static bool readyToContinue; |
| [[WKWebsiteDataStore defaultDataStore] fetchDataRecordsOfTypes:websiteDataTypes.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) { |
| EXPECT_EQ(1U, dataRecords.count); |
| readyToContinue = true; |
| }]; |
| TestWebKitAPI::Util::run(&readyToContinue); |
| readyToContinue = false; |
| |
| // Clear DOM Cache |
| auto typesToRemove = adoptNS([[NSSet alloc] initWithArray:@[WKWebsiteDataTypeFetchCache]]); |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:typesToRemove.get() modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| // Fetch SW records again |
| [[WKWebsiteDataStore defaultDataStore] fetchDataRecordsOfTypes:websiteDataTypes.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) { |
| EXPECT_EQ(0U, dataRecords.count); |
| readyToContinue = true; |
| }]; |
| TestWebKitAPI::Util::run(&readyToContinue); |
| readyToContinue = false; |
| } |
| |
| TEST(ServiceWorkers, CustomDataStorePathsVersusCompletionHandlers) |
| { |
| NSURL *swPath = [NSURL fileURLWithPath:[@"~/Library/Caches/com.apple.WebKit.TestWebKitAPI/WebKit/ServiceWorkers/" stringByExpandingTildeInPath]]; |
| [[NSFileManager defaultManager] removeItemAtURL:swPath error:nil]; |
| EXPECT_FALSE([[NSFileManager defaultManager] fileExistsAtPath:swPath.path]); |
| |
| [[NSFileManager defaultManager] createDirectoryAtURL:swPath withIntermediateDirectories:YES attributes:nil error:nil]; |
| |
| auto websiteDataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| websiteDataStoreConfiguration.get().unifiedOriginStorageLevel = _WKUnifiedOriginStorageLevelBasic; |
| websiteDataStoreConfiguration.get()._serviceWorkerRegistrationDirectory = swPath; |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]); |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [configuration setWebsiteDataStore:dataStore.get()]; |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { mainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, scriptBytes } }, |
| }); |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView loadRequest:server.request()]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| unsigned timeout = 0; |
| while (![[NSFileManager defaultManager] fileExistsAtPath:[swPath URLByAppendingPathComponent:serviceWorkerRegistrationFilename].path] && ++timeout < 20) |
| TestWebKitAPI::Util::runFor(0.1_s); |
| EXPECT_TRUE([[NSFileManager defaultManager] fileExistsAtPath:[swPath URLByAppendingPathComponent:serviceWorkerRegistrationFilename].path]); |
| |
| // Fetch SW records |
| auto websiteDataTypes = adoptNS([[NSSet alloc] initWithArray:@[WKWebsiteDataTypeServiceWorkerRegistrations]]); |
| static bool readyToContinue; |
| [dataStore fetchDataRecordsOfTypes:websiteDataTypes.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) { |
| EXPECT_EQ(1U, dataRecords.count); |
| readyToContinue = true; |
| }]; |
| TestWebKitAPI::Util::run(&readyToContinue); |
| readyToContinue = false; |
| |
| // Fetch records again, this time releasing our reference to the data store while the request is in flight. |
| [dataStore fetchDataRecordsOfTypes:websiteDataTypes.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) { |
| EXPECT_EQ(1U, dataRecords.count); |
| readyToContinue = true; |
| }]; |
| dataStore = nil; |
| TestWebKitAPI::Util::run(&readyToContinue); |
| readyToContinue = false; |
| |
| // Delete all SW records, releasing our reference to the data store while the request is in flight. |
| dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]); |
| [dataStore removeDataOfTypes:websiteDataTypes.get() modifiedSince:[NSDate distantPast] completionHandler:^() { |
| readyToContinue = true; |
| }]; |
| dataStore = nil; |
| TestWebKitAPI::Util::run(&readyToContinue); |
| readyToContinue = false; |
| |
| // The records should have been deleted, and the callback should have been made. |
| // Now refetch the records to verify they are gone. |
| dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:websiteDataStoreConfiguration.get()]); |
| [dataStore fetchDataRecordsOfTypes:websiteDataTypes.get() completionHandler:^(NSArray<WKWebsiteDataRecord *> *dataRecords) { |
| EXPECT_EQ(0U, dataRecords.count); |
| readyToContinue = true; |
| }]; |
| TestWebKitAPI::Util::run(&readyToContinue); |
| } |
| |
| TEST(ServiceWorkers, WebProcessCache) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto processPoolConfiguration = adoptNS([[_WKProcessPoolConfiguration alloc] init]); |
| processPoolConfiguration.get().processSwapsOnNavigation = YES; |
| processPoolConfiguration.get().usesWebProcessCache = YES; |
| processPoolConfiguration.get().prewarmsProcessesAutomatically = YES; |
| auto processPool = adoptNS([[WKProcessPool alloc] _initWithConfiguration:processPoolConfiguration.get()]); |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [configuration setProcessPool:processPool.get()]; |
| |
| auto messageHandler = adoptNS([[SWMessageHandler alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| static constexpr auto main = |
| "<script>" |
| "function registerServiceWorker()" |
| "{" |
| " try {" |
| " navigator.serviceWorker.register('/sw.js').then(function(reg) {" |
| " if (reg.active) {" |
| " alert('worker unexpectedly already active');" |
| " return;" |
| " }" |
| " worker = reg.installing;" |
| " worker.addEventListener('statechange', function() {" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " }).catch(function(error) {" |
| " alert('Registration failed with: ' + error);" |
| " });" |
| " } catch(e) {" |
| " alert('Exception: ' + e);" |
| " }" |
| "}" |
| "alert('loaded');" |
| "</script>"_s; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { main } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, emptyString() } } |
| }); |
| |
| // Create a first web view and load a page |
| auto webView1 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView1 loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "loaded"); |
| |
| // Create a second web view and load a page |
| auto webView2 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView2 loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "loaded"); |
| |
| // Close the first page to put it in the cache |
| [webView1 _close]; |
| |
| // Register a service worker, it should go in webView1 process. |
| [webView2 stringByEvaluatingJavaScript:@"registerServiceWorker()"]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "successfully registered"); |
| |
| // Clearing web process cache should not kill the service worker process |
| [configuration.get().processPool _clearWebProcessCache]; |
| |
| EXPECT_EQ(1U, configuration.get().processPool._serviceWorkerProcessCount); |
| } |
| |
| static bool didStartURLSchemeTask = false; |
| static bool didStartURLSchemeTaskForXMLFile = false; |
| static bool didStartURLSchemeTaskForImportedScript = false; |
| |
| @interface ServiceWorkerSchemeHandler : NSObject <WKURLSchemeHandler> { |
| const char* _bytes; |
| HashMap<String, RetainPtr<NSData>> _dataMappings; |
| } |
| - (instancetype)initWithBytes:(const char*)bytes; |
| - (void)addMappingFromURLString:(NSString *)urlString toData:(const char*)data; |
| @end |
| |
| @implementation ServiceWorkerSchemeHandler |
| |
| - (instancetype)initWithBytes:(const char*)bytes |
| { |
| self = [super init]; |
| _bytes = bytes; |
| return self; |
| } |
| |
| - (void)addMappingFromURLString:(NSString *)urlString toData:(const char*)data |
| { |
| _dataMappings.set(urlString, [NSData dataWithBytes:(void*)data length:strlen(data)]); |
| } |
| |
| - (void)webView:(WKWebView *)webView startURLSchemeTask:(id <WKURLSchemeTask>)task |
| { |
| if ([(id<WKURLSchemeTaskPrivate>)task _requestOnlyIfCached]) { |
| [task didFailWithError:[NSError errorWithDomain:@"TestWebKitAPI" code:1 userInfo:nil]]; |
| return; |
| } |
| |
| NSURL *finalURL = task.request.URL; |
| |
| if (URL(finalURL).string().endsWith("importedScript.js"_s)) |
| didStartURLSchemeTaskForImportedScript = true; |
| |
| if (URL(finalURL).string().endsWith(".xml"_s)) |
| didStartURLSchemeTaskForXMLFile = true; |
| |
| NSMutableDictionary* headerDictionary = [NSMutableDictionary dictionary]; |
| if (URL(finalURL).string().endsWith(".js"_s)) |
| [headerDictionary setObject:@"text/javascript" forKey:@"Content-Type"]; |
| else |
| [headerDictionary setObject:@"text/html" forKey:@"Content-Type"]; |
| [headerDictionary setObject:@"1" forKey:@"Content-Length"]; |
| auto response = adoptNS([[NSHTTPURLResponse alloc] initWithURL:finalURL statusCode:200 HTTPVersion:@"HTTP/1.1" headerFields:headerDictionary]); |
| [task didReceiveResponse:response.get()]; |
| |
| if (RetainPtr data = _dataMappings.get([finalURL absoluteString])) |
| [task didReceiveData:data.get()]; |
| else if (_bytes) |
| [task didReceiveData:toNSData(byteCast<uint8_t>(unsafeSpan(_bytes))).get()]; |
| else |
| [task didReceiveData:[@"Hello" dataUsingEncoding:NSUTF8StringEncoding]]; |
| |
| [task didFinish]; |
| |
| didStartURLSchemeTask = true; |
| } |
| |
| - (void)webView:(WKWebView *)webView stopURLSchemeTask:(id <WKURLSchemeTask>)task |
| { |
| } |
| |
| @end |
| |
| @interface ServiceWorkerPageRemoteObject : NSObject <ServiceWorkerPageProtocol> |
| @end |
| |
| @implementation ServiceWorkerPageRemoteObject |
| |
| - (void)serviceWorkerGlobalObjectIsAvailable |
| { |
| serviceWorkerGlobalObjectIsAvailable = true; |
| } |
| |
| @end |
| |
| TEST(ServiceWorker, ExtensionServiceWorker) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto schemeHandler = adoptNS([ServiceWorkerSchemeHandler new]); |
| [schemeHandler addMappingFromURLString:@"sw-ext://ABC/other.html" toData:"foo"]; |
| [schemeHandler addMappingFromURLString:@"sw-ext://ABC/sw.js" toData:"importScripts('sw-ext://ABC/importedScript.js');"]; |
| [schemeHandler addMappingFromURLString:@"sw-ext://ABC/bar.xml" toData:"bar"]; |
| [schemeHandler addMappingFromURLString:@"sw-ext://ABC/importedScript.js" toData:"fetch('sw-ext://ABC/bar.xml');"]; |
| |
| WKWebViewConfiguration *webViewConfiguration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ServiceWorkerPagePlugIn"]; |
| |
| auto otherViewConfiguration = adoptNS([WKWebViewConfiguration new]); |
| otherViewConfiguration.get().processPool = webViewConfiguration.processPool; |
| [otherViewConfiguration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"sw-ext"]; |
| auto otherWebView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:otherViewConfiguration.get()]); |
| [otherWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"sw-ext://ABC/other.html"]]]; |
| [otherWebView _test_waitForDidFinishNavigation]; |
| |
| auto otherViewPID = [otherWebView _webProcessIdentifier]; |
| |
| webViewConfiguration.websiteDataStore = [otherViewConfiguration websiteDataStore]; |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| webViewConfiguration._relatedWebView = otherWebView.get(); |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| [webViewConfiguration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"sw-ext"]; |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:webViewConfiguration]); |
| |
| auto object = adoptNS([[ServiceWorkerPageRemoteObject alloc] init]); |
| _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(ServiceWorkerPageProtocol)]; |
| [[webView _remoteObjectRegistry] registerExportedObject:object.get() interface:interface]; |
| |
| // The service worker script should get loaded over the custom scheme handler. |
| done = false; |
| didStartURLSchemeTask = false; |
| didStartURLSchemeTaskForXMLFile = false; |
| didStartURLSchemeTaskForImportedScript = false; |
| [webView _loadServiceWorker:[NSURL URLWithString:@"sw-ext://ABC/sw.js"] usingModules:NO completionHandler:^(BOOL success) { |
| EXPECT_TRUE(success); |
| EXPECT_TRUE(didStartURLSchemeTask); |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| |
| // The injected bundle should have been notified that the service worker's global object was available. |
| TestWebKitAPI::Util::run(&serviceWorkerGlobalObjectIsAvailable); |
| |
| // Both views should be sharing the same WebProcess, which should also host the service worker. |
| ASSERT_EQ(otherViewPID, [webView _webProcessIdentifier]); |
| while (!webViewConfiguration.processPool._serviceWorkerProcessCount) |
| TestWebKitAPI::Util::spinRunLoop(10); |
| |
| EXPECT_EQ(webViewConfiguration.processPool._serviceWorkerProcessCount, 1U); |
| EXPECT_EQ(webViewConfiguration.processPool._webProcessCountIgnoringPrewarmedAndCached, 1U); |
| |
| TestWebKitAPI::Util::runFor(0.5_s); |
| |
| EXPECT_EQ(webViewConfiguration.processPool._serviceWorkerProcessCount, 1U); |
| EXPECT_EQ(webViewConfiguration.processPool._webProcessCountIgnoringPrewarmedAndCached, 1U); |
| |
| EXPECT_WK_STREQ([webView URL].absoluteString, @"sw-ext://ABC"); |
| |
| TestWebKitAPI::Util::run(&didStartURLSchemeTaskForImportedScript); |
| TestWebKitAPI::Util::run(&didStartURLSchemeTaskForXMLFile); |
| |
| // The service worker should exit if we close/deallocate the view we used to launch it. |
| [webView _close]; |
| webView = nil; |
| while (webViewConfiguration.processPool._serviceWorkerProcessCount) |
| TestWebKitAPI::Util::spinRunLoop(10); |
| } |
| |
| TEST(ServiceWorker, ExtensionServiceWorkerDisableCORS) |
| { |
| using namespace TestWebKitAPI; |
| |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| Util::run(&done); |
| done = false; |
| |
| bool madeHTTPGetRequest = false; |
| bool madeHTTPOptionsRequest = false; |
| String filenameRequestedOverHTTP; |
| HTTPServer server([&] (Connection connection) { |
| connection.receiveHTTPRequest([&, connection](Vector<char>&& bytes) mutable { |
| String requestString(bytes.span()); |
| if (requestString.startsWithIgnoringASCIICase("OPTIONS"_s)) { |
| madeHTTPOptionsRequest = true; |
| connection.send( |
| "HTTP/1.1 204 No Content\r\n" |
| "Allow: OPTIONS, GET, HEAD, POST\r\n\r\n"_s |
| ); |
| return; |
| } |
| if (requestString.startsWithIgnoringASCIICase("GET"_s)) { |
| auto requestParts = requestString.split(' '); |
| if (requestParts.size() > 2) |
| filenameRequestedOverHTTP = requestParts[1]; |
| madeHTTPGetRequest = true; |
| done = true; |
| } |
| }); |
| }); |
| |
| auto testJS = makeString("fetch('http://127.0.0.1:"_s, server.port(), "/bar.xml', { headers: { 'Custom-Header': 'CustomHeaderValue' } });"_s); |
| |
| auto schemeHandler = adoptNS([ServiceWorkerSchemeHandler new]); |
| [schemeHandler addMappingFromURLString:@"sw-ext://ABC/sw.js" toData:testJS.utf8().data()]; |
| |
| WKWebViewConfiguration *webViewConfiguration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ServiceWorkerPagePlugIn"]; |
| [webViewConfiguration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"sw-ext"]; |
| webViewConfiguration._corsDisablingPatterns = @[@"*://*/*"]; |
| |
| // Adding another world to the page. |
| RetainPtr<WKContentWorld> world = [WKContentWorld worldWithName:@"TestWorld"]; |
| RetainPtr<SWMessageHandler> handler = adoptNS([[SWMessageHandler alloc] init]); |
| RetainPtr<WKUserScript> userScript = adoptNS([[WKUserScript alloc] _initWithSource:@"window.webkit" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO includeMatchPatternStrings:@[] excludeMatchPatternStrings:@[] associatedURL:nil contentWorld:world.get() deferRunningUntilNotification:NO]); |
| [[webViewConfiguration userContentController] _addScriptMessageHandler:handler.get() name:@"testHandler" contentWorld:world.get()]; |
| [[webViewConfiguration userContentController] addUserScript:userScript.get()]; |
| |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:webViewConfiguration]); |
| // The service worker script should get loaded over the custom scheme handler. |
| done = false; |
| didStartURLSchemeTask = false; |
| [webView _loadServiceWorker:[NSURL URLWithString:@"sw-ext://ABC/sw.js"] usingModules:NO completionHandler:^(BOOL success) { |
| EXPECT_TRUE(success); |
| EXPECT_TRUE(didStartURLSchemeTask); |
| done = true; |
| }]; |
| Util::run(&done); |
| |
| // It should load bar.xml. |
| Util::run(&madeHTTPGetRequest); |
| EXPECT_STREQ(filenameRequestedOverHTTP.utf8().data(), "/bar.xml"); |
| |
| // It shouldn't have done a CORS preflight. |
| EXPECT_FALSE(madeHTTPOptionsRequest); |
| } |
| |
| TEST(ServiceWorker, ExtensionServiceWorkerWithModules) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto schemeHandler = adoptNS([ServiceWorkerSchemeHandler new]); |
| |
| [schemeHandler addMappingFromURLString:@"sw-ext://ABC/exports.js" toData:"const x = 805; export { x };"]; |
| [schemeHandler addMappingFromURLString:@"sw-ext://ABC/sw.js" toData:"import { x } from './exports.js'; x;"]; |
| |
| WKWebViewConfiguration *webViewConfiguration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ServiceWorkerPagePlugIn"]; |
| webViewConfiguration.websiteDataStore = [adoptNS([WKWebViewConfiguration new]) websiteDataStore]; |
| [webViewConfiguration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"sw-ext"]; |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:webViewConfiguration]); |
| |
| auto object = adoptNS([[ServiceWorkerPageRemoteObject alloc] init]); |
| _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(ServiceWorkerPageProtocol)]; |
| [[webView _remoteObjectRegistry] registerExportedObject:object.get() interface:interface]; |
| |
| // The service worker script should get loaded over the custom scheme handler. |
| done = false; |
| didStartURLSchemeTask = false; |
| [webView _loadServiceWorker:[NSURL URLWithString:@"sw-ext://ABC/sw.js"] usingModules:YES completionHandler:^(BOOL success) { |
| EXPECT_TRUE(success); |
| EXPECT_TRUE(didStartURLSchemeTask); |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| TEST(ServiceWorker, ExtensionServiceWorkerFailureBadScript) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto schemeHandler = adoptNS([ServiceWorkerSchemeHandler new]); |
| [schemeHandler addMappingFromURLString:@"sw-ext://ABC/bad-sw.js" toData:"1 = 1;"]; |
| |
| WKWebViewConfiguration *webViewConfiguration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ServiceWorkerPagePlugIn"]; |
| [webViewConfiguration setURLSchemeHandler:schemeHandler.get() forURLScheme:@"sw-ext"]; |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:webViewConfiguration]); |
| |
| auto object = adoptNS([[ServiceWorkerPageRemoteObject alloc] init]); |
| _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(ServiceWorkerPageProtocol)]; |
| [[webView _remoteObjectRegistry] registerExportedObject:object.get() interface:interface]; |
| |
| // The service worker script should get loaded over the custom scheme handler. |
| done = false; |
| didStartURLSchemeTask = false; |
| [webView _loadServiceWorker:[NSURL URLWithString:@"sw-ext://ABC/bad-sw.js"] usingModules:NO completionHandler:^(BOOL success) { |
| EXPECT_FALSE(success); |
| EXPECT_TRUE(didStartURLSchemeTask); |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| TEST(ServiceWorker, ExtensionServiceWorkerFailureBadURL) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| WKWebViewConfiguration *webViewConfiguration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ServiceWorkerPagePlugIn"]; |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:webViewConfiguration]); |
| |
| auto object = adoptNS([[ServiceWorkerPageRemoteObject alloc] init]); |
| _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(ServiceWorkerPageProtocol)]; |
| [[webView _remoteObjectRegistry] registerExportedObject:object.get() interface:interface]; |
| |
| done = false; |
| [webView _loadServiceWorker:[NSURL URLWithString:@"https://ABC/does-not-exist.js"] usingModules:NO completionHandler:^(BOOL success) { |
| EXPECT_FALSE(success); |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| TEST(ServiceWorker, ExtensionServiceWorkerFailureViewClosed) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| WKWebViewConfiguration *webViewConfiguration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ServiceWorkerPagePlugIn"]; |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:webViewConfiguration]); |
| |
| auto object = adoptNS([[ServiceWorkerPageRemoteObject alloc] init]); |
| _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(ServiceWorkerPageProtocol)]; |
| [[webView _remoteObjectRegistry] registerExportedObject:object.get() interface:interface]; |
| |
| [webView _loadServiceWorker:[NSURL URLWithString:@"https://ABC/does-not-exist.js"] usingModules:NO completionHandler:^(BOOL success) { |
| EXPECT_FALSE(success); |
| done = true; |
| }]; |
| [webView _close]; |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| TEST(ServiceWorker, ExtensionServiceWorkerFailureViewDestroyed) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| @autoreleasepool { |
| WKWebViewConfiguration *webViewConfiguration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"ServiceWorkerPagePlugIn"]; |
| auto webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:webViewConfiguration]); |
| |
| auto object = adoptNS([[ServiceWorkerPageRemoteObject alloc] init]); |
| _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(ServiceWorkerPageProtocol)]; |
| [[webView _remoteObjectRegistry] registerExportedObject:object.get() interface:interface]; |
| |
| [webView _loadServiceWorker:[NSURL URLWithString:@"https://ABC/does-not-exist.js"] usingModules:NO completionHandler:^(BOOL success) { |
| EXPECT_FALSE(success); |
| done = true; |
| }]; |
| } |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| #if WK_HAVE_C_SPI |
| static constexpr auto RemovalOfSameRegistrableDomainButDifferentOriginMainPage = |
| "<div>test page</div>" |
| "<script>" |
| "async function test() {" |
| " try {" |
| " const registration = await navigator.serviceWorker.register('/sw.js');" |
| " if (registration.active) {" |
| " alert('already active');" |
| " return;" |
| " }" |
| " const worker = registration.installing;" |
| " worker.addEventListener('statechange', () => {" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " } catch(e) {" |
| " alert('Exception: ' + e);" |
| " }" |
| "}" |
| "window.onload = test;" |
| "</script>"_s; |
| TEST(ServiceWorker, RemovalOfSameRegistrableDomainButDifferentOrigin) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [[WKWebsiteDataStore defaultDataStore] _terminateNetworkProcess]; |
| |
| auto localhostAliases = adoptWK(WKMutableArrayCreate()); |
| WKArrayAppendItem(localhostAliases.get(), TestWebKitAPI::Util::toWK("a.amazon.com").get()); |
| WKArrayAppendItem(localhostAliases.get(), TestWebKitAPI::Util::toWK("b.amazon.com").get()); |
| WKContextSetLocalhostAliases(nullptr, localhostAliases.get()); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/main.html"_s, { RemovalOfSameRegistrableDomainButDifferentOriginMainPage } }, |
| { "/other.html"_s, { "<div>other page<div><script>alert('loaded')</script>"_s } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, "onfetch = () => { };"_s } } |
| }); |
| |
| auto mainViewConfiguration = adoptNS([WKWebViewConfiguration new]); |
| [mainViewConfiguration.get().processPool _setUseSeparateServiceWorkerProcess: true]; |
| [mainViewConfiguration.get().preferences _setSecureContextChecksEnabled:NO]; |
| |
| // Load main page which will register the service worker in another process. |
| auto mainWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:mainViewConfiguration.get()]); |
| [mainWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://a.amazon.com:%d/main.html", server.port()]]]]; |
| EXPECT_WK_STREQ([mainWebView _test_waitForAlert], "successfully registered"); |
| |
| [mainWebView _setThrottleStateForTesting:1]; |
| |
| // Load other page with different origin but same registrable domain in the same process as main page. |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| mainViewConfiguration.get()._relatedWebView = mainWebView.get(); |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| auto otherWebView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:mainViewConfiguration.get()]); |
| [otherWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://b.amazon.com:%d/other.html", server.port()]]]]; |
| EXPECT_WK_STREQ([otherWebView _test_waitForAlert], "loaded"); |
| |
| // Navigate other page to remove the client with different origin but same registrable domain. |
| [otherWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:[NSString stringWithFormat:@"http://a.amazon.com:%d/main.html", server.port()]]]]; |
| EXPECT_WK_STREQ([otherWebView _test_waitForAlert], "already active"); |
| |
| // Wait a bit. |
| TestWebKitAPI::Util::runFor(0.5_s); |
| |
| // Verify the process running the service worker is not suspended. |
| ASSERT_TRUE([mainWebView _hasServiceWorkerForegroundActivityForTesting] || [mainWebView _hasServiceWorkerBackgroundActivityForTesting]); |
| } |
| |
| static constexpr auto cacheStorageNetworkProcessCrashMainBytes = R"SWRESOURCE( |
| <html> |
| <body> |
| <div> |
| hello |
| </div> |
| <script> |
| var cache1; |
| onload = async () => { |
| try { |
| // Create cache1 |
| cache1 = await self.caches.open("cache1"); |
| await cache1.put(new Request('my request'), new Response('my response')); |
| webkit.messageHandlers.sw.postMessage("PASS"); |
| } catch(e) { |
| webkit.messageHandlers.sw.postMessage("put failed with " + e); |
| } |
| } |
| |
| // The web process may not know that the network process has been terminated since |
| // this notification happens asynchronously. So we make calls to open a cache. Until the |
| // web process is notified, these calls will fail. Once a call succeeds, we know that the |
| // web process has been notified and has established a new connection to a new network |
| // process. Then, when match is called, we can check that this new process does not |
| // contain cache1, which was created using the now-terminated network process. |
| |
| async function check() |
| { |
| const maxAttempts = 10; |
| let attempt = 1; |
| let cache2 = null; |
| |
| while (attempt <= maxAttempts) { |
| try { |
| cache2 = await self.caches.open("cache2"); |
| break; |
| } catch(e) { |
| attempt++; |
| } |
| } |
| |
| // If the test is flaky due to hitting this error, we likely need to increase maxAttempts. |
| if (!cache2) { |
| webkit.messageHandlers.sw.postMessage("FAIL: web process was not notified that network process was terminated"); |
| return; |
| } |
| |
| try { |
| await cache1.match('my request'); |
| webkit.messageHandlers.sw.postMessage("FAIL: cache1 unexpectedly matched"); |
| } catch(e) { |
| webkit.messageHandlers.sw.postMessage("PASS"); |
| } |
| } |
| </script> |
| </body> |
| </html> |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorkers, CacheStorageNetworkProcessCrash) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| configuration.get().websiteDataStore = [WKWebsiteDataStore nonPersistentDataStore]; |
| |
| auto defaultPreferences = [configuration preferences]; |
| [defaultPreferences _setSecureContextChecksEnabled:NO]; |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerForCacheStorage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { cacheStorageNetworkProcessCrashMainBytes } } |
| }); |
| |
| auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| // Create cache1 |
| [webView loadRequest:server.request()]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [configuration.get().websiteDataStore _terminateNetworkProcess]; |
| |
| // Verify cache1 is invalidated after network process crash. |
| [webView evaluateJavaScript:@"check()" completionHandler: nil]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| #endif // WK_HAVE_C_SPI |
| |
| static constexpr auto ServiceWorkerWindowClientFocusMain = |
| "<div>test page</div>" |
| "<script>" |
| "let worker;" |
| "async function test() {" |
| " try {" |
| " const registration = await navigator.serviceWorker.register('/sw.js');" |
| " if (registration.active) {" |
| " worker = registration.active;" |
| " alert('already active');" |
| " return;" |
| " }" |
| " worker = registration.installing;" |
| " worker.addEventListener('statechange', () => {" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " } catch(e) {" |
| " alert('Exception: ' + e);" |
| " }" |
| "}" |
| "window.onload = test;" |
| "" |
| "function focusClient() {" |
| " worker.postMessage('start');" |
| " navigator.serviceWorker.onmessage = (event) => {" |
| " window.webkit.messageHandlers.sw.postMessage(event.data);" |
| " }" |
| "}" |
| "" |
| "function checkFocusValue(value, name) {" |
| " window.webkit.messageHandlers.sw.postMessage(document.hasFocus() === value ? 'PASS' : 'FAIL: expected ' + value + ' for ' + name);" |
| "}" |
| "</script>"_s; |
| static constexpr auto ServiceWorkerWindowClientFocusJS = |
| "self.addEventListener('message', (event) => {" |
| " event.source.focus().then((client) => {" |
| " event.source.postMessage('focused');" |
| " }, (error) => {" |
| " event.source.postMessage('not focused');" |
| " });" |
| "});"_s; |
| |
| #if PLATFORM(MAC) |
| void miniaturizeWebView(TestWKWebView* webView) |
| { |
| [[webView hostWindow] miniaturize:[webView hostWindow]]; |
| |
| int cptr = 0; |
| while ([webView hostWindow].isVisible && ++cptr < 1000) |
| TestWebKitAPI::Util::spinRunLoop(10); |
| } |
| #endif // PLATFORM(MAC) |
| |
| TEST(ServiceWorker, ServiceWorkerWindowClientFocus) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| auto preferences = [configuration preferences]; |
| |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"ServiceWorkersUserGestureEnabled"]) |
| [preferences _setEnabled:NO forFeature:feature]; |
| } |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { ServiceWorkerWindowClientFocusMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerWindowClientFocusJS } } |
| }); |
| |
| [webView1 loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "successfully registered"); |
| |
| [webView2 loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "already active"); |
| |
| #if PLATFORM(MAC) |
| miniaturizeWebView(webView1.get()); |
| EXPECT_FALSE([webView1 hostWindow].isVisible); |
| |
| miniaturizeWebView(webView2.get()); |
| EXPECT_FALSE([webView2 hostWindow].isVisible); |
| #endif |
| |
| done = false; |
| expectedMessage = "focused"_s; |
| [webView1 evaluateJavaScript:@"focusClient()" completionHandler: nil]; |
| TestWebKitAPI::Util::run(&done); |
| #if PLATFORM(MAC) |
| EXPECT_TRUE([webView1 hostWindow].isVisible); |
| EXPECT_FALSE([webView2 hostWindow].isVisible); |
| while ([webView1 hostWindow].isMiniaturized) |
| TestWebKitAPI::Util::spinRunLoop(1); |
| while (![webView2 hostWindow].isMiniaturized) |
| TestWebKitAPI::Util::spinRunLoop(1); |
| |
| // FIXME: We should be able to run these tests in iOS once pages are actually visible. |
| done = false; |
| expectedMessage = "PASS"_s; |
| [webView1 evaluateJavaScript:@"checkFocusValue(true, 'webView1')" completionHandler:nil]; |
| TestWebKitAPI::Util::run(&done); |
| |
| done = false; |
| expectedMessage = "PASS"_s; |
| [webView2 evaluateJavaScript:@"checkFocusValue(false, 'webView2')" completionHandler:nil]; |
| TestWebKitAPI::Util::run(&done); |
| #endif |
| |
| done = false; |
| expectedMessage = "focused"_s; |
| [webView2 evaluateJavaScript:@"focusClient()" completionHandler: nil]; |
| TestWebKitAPI::Util::run(&done); |
| #if PLATFORM(MAC) |
| EXPECT_TRUE([webView2 hostWindow].isVisible); |
| while ([webView1 hostWindow].isMiniaturized) |
| TestWebKitAPI::Util::spinRunLoop(1); |
| while ([webView2 hostWindow].isMiniaturized) |
| TestWebKitAPI::Util::spinRunLoop(1); |
| |
| // FIXME: We should be able to run these tests in iOS once pages are actually visible. |
| done = false; |
| expectedMessage = "PASS"_s; |
| [webView2 evaluateJavaScript:@"checkFocusValue(true, 'webView2')" completionHandler:nil]; |
| TestWebKitAPI::Util::run(&done); |
| #endif |
| } |
| |
| TEST(ServiceWorker, ServiceWorkerWindowClientFocusRequiresUserGesture) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| auto preferences = [configuration preferences]; |
| |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"ServiceWorkersUserGestureEnabled"]) |
| [preferences _setEnabled:YES forFeature:feature]; |
| } |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { ServiceWorkerWindowClientFocusMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerWindowClientFocusJS } } |
| }); |
| |
| [webView loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "successfully registered"); |
| |
| done = false; |
| expectedMessage = "not focused"_s; |
| [webView evaluateJavaScript:@"focusClient()" completionHandler: nil]; |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| static constexpr auto ServiceWorkerWindowClientOpenWindowMain = |
| "<div id='log'>test page</div>" |
| "<script>" |
| "let worker;" |
| "async function test() {" |
| " log.innerHTML = 'test';" |
| " try {" |
| " const registration = await navigator.serviceWorker.register('/sw.js');" |
| " log.innerHTML = 'registered';" |
| " if (registration.active) {" |
| " worker = registration.active;" |
| " alert('already active');" |
| " return;" |
| " }" |
| " worker = registration.installing;" |
| " worker.addEventListener('statechange', () => {" |
| " log.innerHTML = 'worker.state=' + worker.state;" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " } catch(e) {" |
| " alert('Exception: ' + e);" |
| " }" |
| "}" |
| "window.onload = test;" |
| "" |
| "function openWindowClient() {" |
| " log.innerHTML = 'openWindowClient';" |
| " worker.postMessage('start');" |
| " navigator.serviceWorker.onmessage = (event) => {" |
| " window.webkit.messageHandlers.sw.postMessage(event.data);" |
| " }" |
| "}" |
| "</script>"_s; |
| static constexpr auto ServiceWorkerWindowClientOpenWindowJS = |
| "self.addEventListener('message', (event) => {" |
| " self.clients.openWindow('/sw.js').then((client) => {" |
| " event.source.postMessage(client ? 'opened with client' : 'opened without client');" |
| " }, (error) => {" |
| " event.source.postMessage('not opened');" |
| " });" |
| "});"_s; |
| |
| TEST(ServiceWorker, openWindowWithoutDelegate) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| auto preferences = [configuration preferences]; |
| |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"ServiceWorkersUserGestureEnabled"]) |
| [preferences _setEnabled:NO forFeature:feature]; |
| } |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { ServiceWorkerWindowClientOpenWindowMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerWindowClientOpenWindowJS } } |
| }); |
| |
| [webView loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "successfully registered"); |
| |
| done = false; |
| expectedMessage = "opened without client"_s; |
| [webView evaluateJavaScript:@"openWindowClient()" completionHandler: nil]; |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| static constexpr auto ServiceWorkerWindowClientNavigateMain = |
| "<div>test page</div>" |
| "<script>" |
| "let worker;" |
| "async function registerServiceWorker() {" |
| " try {" |
| " const registration = await navigator.serviceWorker.register('/sw.js');" |
| " if (registration.active) {" |
| " worker = registration.active;" |
| " alert('already active');" |
| " return;" |
| " }" |
| " worker = registration.installing;" |
| " worker.addEventListener('statechange', () => {" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " } catch(e) {" |
| " alert('Exception: ' + e);" |
| " }" |
| "}" |
| "window.onload = registerServiceWorker;" |
| "" |
| "function navigateOtherClientToURL(url) {" |
| " worker.postMessage({navigateOtherClientToURL: url});" |
| " navigator.serviceWorker.onmessage = (event) => {" |
| " alert(event.data);" |
| " };" |
| "}" |
| "" |
| "function countServiceWorkerClients() {" |
| " worker.postMessage('countServiceWorkerClients');" |
| " navigator.serviceWorker.onmessage = (event) => {" |
| " alert(event.data);" |
| " };" |
| "}" |
| "" |
| "function openWindowToURL(url) {" |
| " worker.postMessage({openWindowToURL: url});" |
| " navigator.serviceWorker.onmessage = (event) => {" |
| " alert(event.data);" |
| " };" |
| "}" |
| "</script>"_s; |
| |
| static constexpr auto ServiceWorkerWindowClientNavigateJS = |
| "self.addEventListener('message', async (event) => {" |
| " if (event.data && event.data.navigateOtherClientToURL) {" |
| " let otherClient;" |
| " let currentClients = await self.clients.matchAll();" |
| " for (let client of currentClients) {" |
| " if (client.id !== event.source.id)" |
| " otherClient = client;" |
| " }" |
| " if (!otherClient) {" |
| " event.source.postMessage('failed, no other client, client number = ' + currentClients.length);" |
| " return;" |
| " }" |
| " await otherClient.navigate(event.data.navigateOtherClientToURL).then((client) => {" |
| " event.source.postMessage(client ? 'client' : 'none');" |
| " }, (e) => {" |
| " event.source.postMessage('failed');" |
| " });" |
| " return;" |
| " }" |
| " if (event.data === 'countServiceWorkerClients') {" |
| " let currentClients = await self.clients.matchAll();" |
| " event.source.postMessage(currentClients.length + ' client(s)');" |
| " return;" |
| " }" |
| " if (event.data && event.data.openWindowToURL) {" |
| " await self.clients.openWindow(event.data.openWindowToURL).then((client) => {" |
| " event.source.postMessage(client ? 'client' : 'none');" |
| " }, (e) => {" |
| " event.source.postMessage('failed');" |
| " });" |
| " return;" |
| " }" |
| "});"_s; |
| |
| static bool shouldServiceWorkerPSONNavigationDelegateAllowNavigation = true; |
| static bool shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = true; |
| @interface ServiceWorkerPSONNavigationDelegate : NSObject <WKNavigationDelegatePrivate> { |
| @public void (^decidePolicyForNavigationAction)(WKNavigationAction *, void (^)(WKNavigationActionPolicy)); |
| @public void (^didStartProvisionalNavigationHandler)(); |
| @public void (^didCommitNavigationHandler)(); |
| } |
| @end |
| |
| @implementation ServiceWorkerPSONNavigationDelegate |
| |
| - (instancetype) init |
| { |
| self = [super init]; |
| return self; |
| } |
| |
| - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler |
| { |
| TestWebKitAPI::Util::runFor(0.01_s); |
| decisionHandler(shouldServiceWorkerPSONNavigationDelegateAllowNavigation ? WKNavigationActionPolicyAllow : WKNavigationActionPolicyCancel); |
| } |
| |
| - (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler |
| { |
| decisionHandler(shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse ? WKNavigationResponsePolicyAllow : WKNavigationResponsePolicyCancel); |
| } |
| |
| @end |
| |
| TEST(ServiceWorker, WindowClientNavigate) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"CrossOriginOpenerPolicyEnabled"]) |
| [[configuration preferences] _setEnabled:YES forFeature:feature]; |
| else if ([feature.key isEqualToString:@"CrossOriginEmbedderPolicyEnabled"]) |
| [[configuration preferences] _setEnabled:YES forFeature:feature]; |
| } |
| |
| auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/?test"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/?fail1"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/?fail2"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/?swap"_s, { {{ "Content-Type"_s, "application/html"_s }, { "Cross-Origin-Opener-Policy"_s, "same-origin"_s } }, ServiceWorkerWindowClientNavigateMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerWindowClientNavigateJS } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Http, nullptr, nullptr, 8091); |
| |
| [webView1 loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "successfully registered"); |
| [webView2 loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "already active"); |
| |
| auto navigationDelegate = adoptNS([[ServiceWorkerPSONNavigationDelegate alloc] init]); |
| [webView2 setNavigationDelegate:navigationDelegate.get()]; |
| |
| auto *baseURL = [[server.request() URL] absoluteString]; |
| |
| [webView1 evaluateJavaScript:[NSString stringWithFormat:@"navigateOtherClientToURL('%@')", baseURL] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "client"); |
| |
| [webView1 evaluateJavaScript:[NSString stringWithFormat:@"navigateOtherClientToURL('%@#test')", baseURL] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "client"); |
| |
| [webView1 evaluateJavaScript:[NSString stringWithFormat:@"navigateOtherClientToURL('%@?test')", baseURL] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "client"); |
| |
| [webView1 evaluateJavaScript:[NSString stringWithFormat:@"navigateOtherClientToURL('%@?swap')", baseURL] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "client"); |
| |
| [webView1 evaluateJavaScript:@"countServiceWorkerClients()" completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "1 client(s)"); |
| |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigation = false; |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = true; |
| [webView1 evaluateJavaScript:[NSString stringWithFormat:@"navigateOtherClientToURL('%@?fail1')", baseURL] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "none"); |
| |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigation = true; |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = false; |
| [webView1 evaluateJavaScript:[NSString stringWithFormat:@"navigateOtherClientToURL('%@?fail2')", baseURL] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "none"); |
| |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigation = true; |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = true; |
| } |
| |
| TEST(ServiceWorker, WindowClientNavigateCrossOrigin) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| TestWebKitAPI::HTTPServer server1({ |
| { "/"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/?test"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/?swap"_s, { {{ "Content-Type"_s, "application/html"_s }, { "Cross-Origin-Opener-Policy"_s, "same-origin"_s } }, ServiceWorkerWindowClientNavigateMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerWindowClientNavigateJS } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Http, nullptr, nullptr, 8091); |
| |
| TestWebKitAPI::HTTPServer server2({ |
| { "/"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerWindowClientNavigateJS } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Http, nullptr, nullptr, 9091); |
| |
| [webView1 loadRequest:server1.request()]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "successfully registered"); |
| [webView2 loadRequest:server1.request()]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "already active"); |
| [webView1 evaluateJavaScript:[NSString stringWithFormat:@"navigateOtherClientToURL('%@')", [[server2.request() URL] absoluteString]] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "none"); |
| } |
| |
| @interface ServiceWorkerOpenWindowWebsiteDataStoreDelegate: NSObject <_WKWebsiteDataStoreDelegate> { |
| @private |
| WKWebViewConfiguration* _configuration; |
| RetainPtr<ServiceWorkerPSONNavigationDelegate> _navigationDelegate; |
| RetainPtr<TestWKWebView> _webView; |
| } |
| - (instancetype)initWithConfiguration:(WKWebViewConfiguration*)configuration; |
| @end |
| |
| @implementation ServiceWorkerOpenWindowWebsiteDataStoreDelegate { } |
| - (instancetype)initWithConfiguration:(WKWebViewConfiguration*)configuration |
| { |
| _configuration = configuration; |
| return self; |
| } |
| |
| - (void)websiteDataStore:(WKWebsiteDataStore *)dataStore openWindow:(NSURL *)url fromServiceWorkerOrigin:(WKSecurityOrigin *)serviceWorkerOrigin completionHandler:(void (^)(WKWebView *newWebView))completionHandler |
| { |
| _navigationDelegate = adoptNS([[ServiceWorkerPSONNavigationDelegate alloc] init]); |
| _webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:_configuration addToWindow:YES]); |
| [_webView setNavigationDelegate:_navigationDelegate.get()]; |
| |
| [_webView loadRequest:[NSURLRequest requestWithURL:url]]; |
| completionHandler(_webView.get()); |
| } |
| |
| @end |
| |
| TEST(ServiceWorker, OpenWindowWebsiteDataStoreDelegate) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto preferences = [configuration preferences]; |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"ServiceWorkersUserGestureEnabled"]) |
| [preferences _setEnabled:NO forFeature:feature]; |
| } |
| |
| auto dataStoreDelegate = adoptNS([[ServiceWorkerOpenWindowWebsiteDataStoreDelegate alloc] initWithConfiguration:configuration.get()]); |
| [[configuration websiteDataStore] set_delegate:dataStoreDelegate.get()]; |
| |
| auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerWindowClientNavigateJS } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Http, nullptr, nullptr, 8091); |
| |
| [webView loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "successfully registered"); |
| |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigation = true; |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = true; |
| [webView evaluateJavaScript:[NSString stringWithFormat:@"openWindowToURL('%@')", [[server.request() URL] absoluteString]] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "client"); |
| |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigation = false; |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = true; |
| [webView evaluateJavaScript:[NSString stringWithFormat:@"openWindowToURL('%@')", [[server.request() URL] absoluteString]] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "none"); |
| |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigation = true; |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = false; |
| [webView evaluateJavaScript:[NSString stringWithFormat:@"openWindowToURL('%@')", [[server.request() URL] absoluteString]] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "none"); |
| |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigation = true; |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = true; |
| } |
| |
| TEST(ServiceWorker, OpenWindowCOOP) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto preferences = [configuration preferences]; |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"ServiceWorkersUserGestureEnabled"]) |
| [preferences _setEnabled:NO forFeature:feature]; |
| } |
| |
| auto dataStoreDelegate = adoptNS([[ServiceWorkerOpenWindowWebsiteDataStoreDelegate alloc] initWithConfiguration:configuration.get()]); |
| [[configuration websiteDataStore] set_delegate:dataStoreDelegate.get()]; |
| |
| auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { ServiceWorkerWindowClientNavigateMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerWindowClientNavigateJS } }, |
| { "/?swap"_s, { {{ "Content-Type"_s, "application/html"_s }, { "Cross-Origin-Opener-Policy"_s, "same-origin"_s } }, ServiceWorkerWindowClientNavigateMain } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Http, nullptr, nullptr, 8091); |
| |
| [webView loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "successfully registered"); |
| |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigation = true; |
| shouldServiceWorkerPSONNavigationDelegateAllowNavigationResponse = true; |
| [webView evaluateJavaScript:[NSString stringWithFormat:@"openWindowToURL('%@?swap')", [[server.request() URL] absoluteString]] completionHandler: nil]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "client"); |
| } |
| |
| #if PLATFORM(MAC) |
| static constexpr auto ServiceWorkerNotYetFocusedMain = |
| "<div>Test page 1</div>" |
| "<script>" |
| "let worker;" |
| "async function registerServiceWorker() {" |
| " try {" |
| " const registration = await navigator.serviceWorker.register('/sw.js');" |
| " if (registration.active) {" |
| " worker = registration.active;" |
| " alert('already active');" |
| " return;" |
| " }" |
| " worker = registration.installing;" |
| " worker.addEventListener('statechange', () => {" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " } catch(e) {" |
| " alert('Exception: ' + e);" |
| " }" |
| "}" |
| "" |
| "function getFocusResult(counter) {" |
| " try {" |
| " if (!counter)" |
| " counter = 0;" |
| " worker.postMessage('getFocusResult');" |
| " navigator.serviceWorker.onmessage = async event => {" |
| " if (event.data !== 'none') {" |
| " alert(event.data);" |
| " return;" |
| " }" |
| " if (counter > 100) {" |
| " alert('focus timed out');" |
| " return;" |
| " }" |
| " await new Promise(resolve => setTimeout(resolve, 50));" |
| " getFocusResult(++counter);" |
| " };" |
| " } catch (e) {" |
| " alert(e);" |
| " }" |
| "}" |
| "window.onload = registerServiceWorker;" |
| "</script>"_s; |
| |
| static constexpr auto ServiceWorkerNotYetFocusedClient = |
| "<div>Test page 2</div>" |
| "<script>" |
| "async function checkFocused() {" |
| " let counter = 0;" |
| " while (++counter < 100) {" |
| " if (document.hasFocus()) {" |
| " alert('PASS');" |
| " return;" |
| " }" |
| " await new Promise(resolve => setTimeout(resolve, 50));" |
| " }" |
| " alert('FAIL');" |
| "}" |
| "window.onload = checkFocused;" |
| "</script>"_s; |
| |
| static constexpr auto ServiceWorkerNotYetFocusedJS = |
| "var focusResult = 'none';" |
| "self.addEventListener('message', event => {" |
| " event.source.postMessage(focusResult);" |
| "});" |
| "" |
| "async function serveContent(event) {" |
| " try {" |
| " const client = await self.clients.get(event.resultingClientId);" |
| " client.focus().then(() => focusResult = 'PASS focus', () => focusResult = 'FAIL focus');" |
| " } catch (e) {" |
| " return new Response('<div>Error page</div><script>alert(Response);</script>', {headers: {'Content-Type': 'text/html'} });" |
| " }" |
| " return fetch(event.request);" |
| "}" |
| "self.addEventListener('fetch', event => {" |
| " event.respondWith(serveContent(event));" |
| "});"_s; |
| |
| TEST(ServiceWorker, FocusNotYetLoadedClient) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto preferences = [configuration preferences]; |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"ServiceWorkersUserGestureEnabled"]) |
| [preferences _setEnabled:NO forFeature:feature]; |
| } |
| |
| auto dataStoreDelegate = adoptNS([[ServiceWorkerOpenWindowWebsiteDataStoreDelegate alloc] initWithConfiguration:configuration.get()]); |
| [[configuration websiteDataStore] set_delegate:dataStoreDelegate.get()]; |
| |
| TestWebKitAPI::HTTPServer server(TestWebKitAPI::HTTPServer::UseCoroutines::Yes, [&](auto connection) -> TestWebKitAPI::ConnectionTask { |
| while (1) { |
| auto request = co_await connection.awaitableReceiveHTTPRequest(); |
| auto path = TestWebKitAPI::HTTPServer::parsePath(request); |
| if (path == "/"_s) { |
| co_await connection.awaitableSend(makeString("HTTP/1.1 200 OK\r\nContent-Type:text/html\r\nContent-Length: "_s, strlen(ServiceWorkerNotYetFocusedMain), "\r\n\r\n"_s)); |
| co_await connection.awaitableSend(ServiceWorkerNotYetFocusedMain); |
| continue; |
| } |
| if (path == "/sw.js"_s) { |
| co_await connection.awaitableSend(makeString("HTTP/1.1 200 OK\r\nContent-Type:application/javascript\r\nContent-Length: "_s, strlen(ServiceWorkerNotYetFocusedJS), "\r\n\r\n"_s)); |
| co_await connection.awaitableSend(ServiceWorkerNotYetFocusedJS); |
| continue; |
| } |
| if (path == "/delayed-client"_s) { |
| TestWebKitAPI::Util::runFor(0.5_s); |
| co_await connection.awaitableSend(makeString("HTTP/1.1 200 OK\r\nContent-Type:text/html\r\nContent-Length: "_s, strlen(ServiceWorkerNotYetFocusedClient), "\r\n\r\n"_s)); |
| co_await connection.awaitableSend(ServiceWorkerNotYetFocusedClient); |
| continue; |
| } |
| EXPECT_FALSE(true); |
| } |
| }); |
| |
| auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| [webView1 loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "successfully registered"); |
| |
| auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| [webView2 loadRequest:server.request("/delayed-client"_s)]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "PASS"); |
| |
| [webView1 evaluateJavaScript:@"getFocusResult()" completionHandler:^(id value, NSError *error) { |
| EXPECT_NULL(error); |
| }]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "PASS focus"); |
| } |
| #endif |
| |
| #if WK_HAVE_C_SPI |
| |
| static constexpr auto serviceWorkerStorageTimingMainBytes = R"SWRESOURCE( |
| <script> |
| function log(msg) |
| { |
| window.webkit.messageHandlers.sw.postMessage(msg); |
| } |
| |
| navigator.serviceWorker.addEventListener("message", function(event) { |
| log("Message from worker: " + event.data); |
| }); |
| |
| let registration; |
| try { |
| navigator.serviceWorker.register('/sw.js').then(function(reg) { |
| registration = reg; |
| worker = reg.installing ? reg.installing : reg.active; |
| worker.postMessage("Hello from the web page"); |
| }).catch(function(error) { |
| log("Registration failed with: " + error); |
| }); |
| } catch(e) { |
| log("Exception: " + e); |
| } |
| |
| function storeRegistration() |
| { |
| if (!window.internals) { |
| alert("no internals"); |
| return; |
| } |
| internals.storeRegistrationsOnDisk().then(() => { |
| alert("ok"); |
| }, () => { |
| alert("ko"); |
| }); |
| } |
| |
| function waitForWaitingWorker(counter) |
| { |
| try { |
| if (registration.waiting) { |
| alert("ok"); |
| return; |
| } |
| if (!counter) |
| counter = 0; |
| else if (counter > 100) { |
| alert("ko"); |
| return; |
| } |
| setTimeout(() => waitForWaitingWorker(++counter), 50); |
| } catch (e) { |
| alert("error: " + e); |
| return; |
| } |
| } |
| |
| </script> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto serviceWorkerStorageTimingScriptBytesV1 = R"SWRESOURCE( |
| self.addEventListener("message", (event) => { |
| event.source.postMessage("V1"); |
| }); |
| )SWRESOURCE"_s; |
| |
| static constexpr auto serviceWorkerStorageTimingScriptBytesV2 = R"SWRESOURCE( |
| self.addEventListener("message", (event) => { |
| event.source.postMessage("V2"); |
| }); |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorkers, ServiceWorkerStorageTiming) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerWithExpectedMessage alloc] init]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { { { "Cache-Control"_s, "no-cache"_s } }, serviceWorkerStorageTimingMainBytes } }, |
| { "/sw.js"_s, { { { "Content-Type"_s, "application/javascript"_s } }, serviceWorkerStorageTimingScriptBytesV1 } }, |
| }); |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| done = false; |
| expectedMessage = "Message from worker: V1"_s; |
| auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get() addToWindow: YES]); |
| [webView1 loadRequest:server.request()]; |
| TestWebKitAPI::Util::run(&done); |
| |
| HashMap<String, String> sourceHeaders; |
| sourceHeaders.add("Cache-Control"_s, "no-cache"_s); |
| sourceHeaders.add("Content-Type"_s, "application/javascript"_s); |
| server.setResponse("/sw.js"_s, TestWebKitAPI::HTTPResponse { WTF::move(sourceHeaders), serviceWorkerStorageTimingScriptBytesV2 }); |
| |
| done = false; |
| expectedMessage = "Message from worker: V1"_s; |
| auto webView2 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView2 loadRequest:server.request()]; |
| TestWebKitAPI::Util::run(&done); |
| |
| // Let's wait for a V2 service worker. |
| [webView1 evaluateJavaScript:@"waitForWaitingWorker()" completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "ok"); |
| |
| // Let's ensure we store it on disk. |
| [webView1 evaluateJavaScript:@"storeRegistration()" completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "ok"); |
| |
| [[webView1 configuration].websiteDataStore _terminateNetworkProcess]; |
| |
| done = false; |
| expectedMessage = "Message from worker: V2"_s; |
| auto webView3 = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView3 loadRequest:server.request()]; |
| TestWebKitAPI::Util::run(&done); |
| } |
| |
| static constexpr auto ServiceWorkerCOOPNavigateMain = |
| "<div>test page</div>" |
| "<script>" |
| "let worker;" |
| "async function registerServiceWorker() {" |
| " try {" |
| " const registration = await navigator.serviceWorker.register('/sw.js');" |
| " if (registration.active) {" |
| " worker = registration.active;" |
| " alert('already active');" |
| " return;" |
| " }" |
| " worker = registration.installing;" |
| " worker.addEventListener('statechange', () => {" |
| " if (worker.state == 'activated')" |
| " alert('successfully registered');" |
| " });" |
| " } catch(e) {" |
| " alert('Exception: ' + e);" |
| " }" |
| "}" |
| "window.onload = registerServiceWorker;" |
| "" |
| "function storeRegistration()" |
| "{" |
| " if (!window.internals) {" |
| " alert('no internals');" |
| " return;" |
| " }" |
| " internals.storeRegistrationsOnDisk().then(() => {" |
| " alert('ok');" |
| " }, () => {" |
| " alert('ko');" |
| " });" |
| "}" |
| "</script>"_s; |
| |
| static constexpr auto ServiceWorkerCOOPNavigateJS = |
| "self.addEventListener('fetch', (event) => {" |
| "});"_s; |
| |
| TEST(ServiceWorker, ServiceWorkerProcessSwapWithNoDelay) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto dataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| [dataStoreConfiguration setServiceWorkerProcessTerminationDelayEnabled:NO]; |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| configuration.get().websiteDataStore = dataStore.get(); |
| |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"CrossOriginOpenerPolicyEnabled"]) |
| [[configuration preferences] _setEnabled:YES forFeature:feature]; |
| else if ([feature.key isEqualToString:@"CrossOriginEmbedderPolicyEnabled"]) |
| [[configuration preferences] _setEnabled:YES forFeature:feature]; |
| } |
| |
| auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| size_t responsePolicyCount { 0 }; |
| auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| auto navigationDelegate = adoptNS([TestNavigationDelegate new]); |
| navigationDelegate.get().decidePolicyForNavigationResponse = makeBlockPtr([&](WKNavigationResponse *, void (^completionHandler)(WKNavigationResponsePolicy)) { |
| ++responsePolicyCount; |
| completionHandler(WKNavigationResponsePolicyAllow); |
| }).get(); |
| webView2.get().navigationDelegate = navigationDelegate.get(); |
| |
| TestWebKitAPI::HTTPServer server(TestWebKitAPI::HTTPServer::UseCoroutines::Yes, [&](auto connection) -> TestWebKitAPI::ConnectionTask { |
| while (1) { |
| auto request = co_await connection.awaitableReceiveHTTPRequest(); |
| auto path = TestWebKitAPI::HTTPServer::parsePath(request); |
| if (path == "/"_s) { |
| co_await connection.awaitableSend(makeString("HTTP/1.1 200 OK\r\nContent-Type:text/html\r\nContent-Length: "_s, strlen(ServiceWorkerCOOPNavigateMain), "\r\n\r\n"_s)); |
| co_await connection.awaitableSend(ServiceWorkerCOOPNavigateMain); |
| continue; |
| } |
| if (path == "/sw.js"_s) { |
| co_await connection.awaitableSend(makeString("HTTP/1.1 200 OK\r\nContent-Type:application/javascript\r\nContent-Length: "_s, strlen(ServiceWorkerCOOPNavigateJS), "\r\n\r\n"_s)); |
| co_await connection.awaitableSend(ServiceWorkerCOOPNavigateJS); |
| continue; |
| } |
| if (path == "/?swap"_s) { |
| TestWebKitAPI::Util::runFor(0.5_s); |
| size_t contentLength = 4000000 + strlen(ServiceWorkerCOOPNavigateMain); |
| co_await connection.awaitableSend(makeString("HTTP/1.1 200 OK\r\nCross-Origin-Opener-Policy:same-origin\r\nContent-Type:text/html\r\nContent-Length: "_s, contentLength, "\r\n\r\n"_s)); |
| co_await connection.awaitableSend(Vector<uint8_t>(4000000, ' ')); |
| |
| while (responsePolicyCount <= 1) |
| TestWebKitAPI::Util::spinRunLoop(); |
| |
| // Wait some time to let potential cancellation have an impact on the load. |
| TestWebKitAPI::Util::runFor(0.5_s); |
| co_await connection.awaitableSend(ServiceWorkerCOOPNavigateMain); |
| continue; |
| } |
| EXPECT_FALSE(true); |
| } |
| }); |
| TestWebKitAPI::HTTPServer server2({ |
| { "/"_s, { ServiceWorkerCOOPNavigateMain } }, |
| { "/?swap"_s, { {{ "Content-Type"_s, "text/html"_s }, { "Cross-Origin-Opener-Policy"_s, "same-origin"_s } }, ServiceWorkerCOOPNavigateMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerCOOPNavigateJS } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Http, nullptr, nullptr, 8092); |
| |
| [webView1 loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "successfully registered"); |
| [webView2 loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://localhost:8092/"]]]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "successfully registered"); |
| |
| [webView1 evaluateJavaScript:@"storeRegistration()" completionHandler: nil]; |
| EXPECT_WK_STREQ([webView1 _test_waitForAlert], "ok"); |
| [webView1 _close]; |
| webView1 = nullptr; |
| |
| [dataStore _terminateNetworkProcess]; |
| |
| [webView2 evaluateJavaScript:[NSString stringWithFormat:@"window.location = '%@?swap';", [[server.request() URL] absoluteString]] completionHandler:^(id value, NSError *error) { |
| EXPECT_NULL(error); |
| }]; |
| EXPECT_WK_STREQ([webView2 _test_waitForAlert], "already active"); |
| } |
| |
| static constexpr auto serviceWorkerCacheReferenceMainBytes = R"SWRESOURCE( |
| <html> |
| <body> |
| <script> |
| async function fill() |
| { |
| try { |
| const cache = await self.caches.open("test"); |
| await cache.put(new Request('test'), new Response('my response')); |
| webkit.messageHandlers.sw.postMessage("PASS"); |
| } catch(e) { |
| webkit.messageHandlers.sw.postMessage("fill failed with " + e); |
| } |
| } |
| |
| async function check() |
| { |
| try { |
| const responsePromise = self.caches.match('test'); |
| // This might trigger origin storage manager cleanup. |
| if (window.internals) |
| internals.cacheStorageEngineRepresentation(); |
| |
| const response = await responsePromise; |
| const result = await response.text() === 'my response'; |
| webkit.messageHandlers.sw.postMessage(result ? "PASS" : "Failed retrieving the response"); |
| } catch(e) { |
| webkit.messageHandlers.sw.postMessage("test failed with " + e); |
| } |
| } |
| |
| onload = () => { |
| if (window.location.hash === "#fill") |
| fill(); |
| else |
| check(); |
| } |
| |
| </script> |
| </body> |
| </html> |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorkers, ServiceWorkerCacheReference) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| |
| auto context = adoptWK(TestWebKitAPI::Util::createContextForInjectedBundleTest("InternalsInjectedBundleTest")); |
| [configuration setProcessPool:(WKProcessPool *)context.get()]; |
| |
| auto defaultPreferences = [configuration preferences]; |
| [defaultPreferences _setSecureContextChecksEnabled:NO]; |
| |
| // We disable local storage to ensure only DOMCache can disable removal of OriginStorageManager. |
| for (_WKFeature *feature in [WKPreferences _features]) { |
| if ([feature.key isEqualToString:@"LocalStorageEnabled"]) |
| [defaultPreferences _setEnabled:NO forFeature:feature]; |
| } |
| |
| auto messageHandler = adoptNS([[SWMessageHandlerForCacheStorage alloc] init]); |
| [[configuration userContentController] addScriptMessageHandler:messageHandler.get() name:@"sw"]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { serviceWorkerCacheReferenceMainBytes } }, |
| { "/clear"_s, { "<script>webkit.messageHandlers.sw.postMessage('PASS');</script>"_s } }, |
| }); |
| |
| auto webView1 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView1 loadRequest:server.request("#fill"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| [webView1 _close]; |
| |
| auto webView2 = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView2 loadRequest:server.request("#check"_s)]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| } |
| |
| static constexpr auto ServiceWorkerIdleOnMemoryPressureMain = R"SWRESOURCE( |
| <!doctype html> |
| <html> |
| <body> |
| <script> |
| function waitForState(worker, state) |
| { |
| if (!worker || worker.state == undefined) |
| return Promise.reject(new Error('waitForState must be passed a ServiceWorker')); |
| |
| if (worker.state === state) |
| return Promise.resolve(state); |
| |
| return new Promise(function(resolve, reject) { |
| worker.addEventListener('statechange', function() { |
| if (worker.state === state) |
| resolve(state); |
| }); |
| setTimeout(() => reject("waitForState timed out, worker state is " + worker.state), 5000); |
| }); |
| } |
| |
| async function startServiceWorker(scope, hasActivity) |
| { |
| const registration = await navigator.serviceWorker.register("sw.js", { scope }); |
| const worker = registration.installing; |
| await waitForState(worker, "activated"); |
| |
| if (hasActivity) { |
| worker.postMessage("ping"); |
| await new Promise((resolve, reject) => { |
| navigator.serviceWorker.onmessage = resolve; |
| setTimeout(() => reject('no pong'), 1000); |
| }); |
| } |
| return worker; |
| } |
| |
| function waitForTermination(worker) |
| { |
| return new Promise(resolve => { |
| internals.whenServiceWorkerIsTerminated(worker).then(() => { |
| resolve(true); |
| }); |
| setTimeout(() => resolve(false), 1000); |
| }); |
| } |
| |
| let promise1, promise2; |
| onload = async () => { |
| let worker1, worker2; |
| try { |
| worker1 = await startServiceWorker("test1", false); |
| worker2 = await startServiceWorker("test2", true); |
| } catch (e) { |
| alert("startServiceWorker failed " + e); |
| } |
| |
| // We need to wait two seconds so that worker1 is idle at memory pressure time. |
| await new Promise(resolve => setTimeout(resolve, 2000)); |
| |
| promise1 = waitForTermination(worker1); |
| promise2 = waitForTermination(worker2); |
| |
| alert("Ready"); |
| } |
| |
| async function checkServiceWorkers() |
| { |
| const test1 = await promise1; |
| if (!test1) { |
| alert("test1 failed"); |
| return; |
| } |
| const test2 = await promise2; |
| alert(test2 ? "test2 failed" : "PASS"); |
| } |
| |
| function doTest() |
| { |
| checkServiceWorkers(); |
| } |
| </script> |
| </body> |
| </html> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto ServiceWorkerIdleOnMemoryPressureJS = R"SWRESOURCE( |
| onmessage = e => { |
| e.source.postMessage("pomg"); |
| e.waitUntil(new Promise(resolve => setTimeout(resolve, 4000))); |
| } |
| )SWRESOURCE"_s; |
| |
| TEST(ServiceWorker, ServiceWorkerIdleOnMemoryPressure) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto dataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| [dataStoreConfiguration setServiceWorkerProcessTerminationDelayEnabled:NO]; |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| configuration.get().websiteDataStore = dataStore.get(); |
| |
| auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { ServiceWorkerIdleOnMemoryPressureMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerIdleOnMemoryPressureJS } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Http, nullptr, nullptr, 8091); |
| |
| [webView loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "Ready"); |
| |
| // Need to trigger critical memory pressure. |
| [webView _terminateIdleServiceWorkersForTesting]; |
| |
| [webView evaluateJavaScript:@"doTest()" completionHandler: nil]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "PASS"); |
| } |
| |
| static constexpr auto ServiceWorkerReadableStreamDownloadMain = R"SWRESOURCE( |
| <!doctype html> |
| <html> |
| <body> |
| <a id=downloadA href="/test1/download">Start download</a> |
| <script> |
| function waitForState(worker, state) |
| { |
| if (!worker || worker.state == undefined) |
| return Promise.reject(new Error('waitForState must be passed a ServiceWorker')); |
| |
| if (worker.state === state) |
| return Promise.resolve(state); |
| |
| return new Promise(function(resolve, reject) { |
| worker.addEventListener('statechange', function() { |
| if (worker.state === state) |
| resolve(state); |
| }); |
| setTimeout(() => reject("waitForState timed out, worker state is " + worker.state), 5000); |
| }); |
| } |
| |
| onload = async () => { |
| try { |
| const registration = await navigator.serviceWorker.register("sw.js", { scope: "/" }); |
| const worker = registration.installing; |
| await waitForState(worker, "activated"); |
| } catch (e) { |
| alert("startServiceWorker failed " + e); |
| } |
| |
| alert("Ready"); |
| } |
| |
| function doTest() |
| { |
| downloadA.click(); |
| |
| new Promise(resolve => { |
| navigator.serviceWorker.onmessage = e => resolve(e.data); |
| setTimeout(() => resolve("Message timed out"), 5000); |
| }).then(data => alert(data)); |
| } |
| </script> |
| </body> |
| </html> |
| )SWRESOURCE"_s; |
| |
| static constexpr auto ServiceWorkerReadableStreamDownloadJS = R"SWRESOURCE( |
| function gc() { |
| if (typeof GCController !== "undefined") |
| GCController.collect(); |
| else { |
| var gcRec = function (n) { |
| if (n < 1) |
| return {}; |
| var temp = {i: "ab" + i + (i / 100000)}; |
| temp += "foo"; |
| gcRec(n-1); |
| }; |
| for (var i = 0; i < 1000; i++) |
| gcRec(10); |
| } |
| } |
| |
| self.addEventListener("fetch", (event) => { |
| event.respondWith(streamResponse()); |
| gc(); |
| setTimeout(gc, 50); |
| }); |
| |
| function streamResponse() { |
| const chunkSize = 1024 * 1024; |
| const chunk = new Uint8Array(chunkSize); |
| let chunksSent = 0; |
| const totalChunks = 10; |
| const stream = new ReadableStream({ |
| start(controller) { |
| function pushChunk() { |
| if (chunksSent < totalChunks) { |
| controller.enqueue(chunk); |
| chunksSent++; |
| setTimeout(pushChunk, 500); |
| } else { |
| controller.close(); |
| } |
| } |
| pushChunk(); |
| }, |
| async cancel() { |
| const currentClients = await self.clients.matchAll({ includeUncontrolled:true }); |
| for (const client of currentClients) |
| client.postMessage("Cancelled"); |
| } |
| }); |
| return new Response(stream, { |
| headers: { |
| "Content-Type": "application/octet-stream", |
| "Content-Disposition": 'attachment; filename="service-worker-download.bin"' |
| } |
| }); |
| })SWRESOURCE"_s; |
| |
| TEST(ServiceWorker, ServiceWorkerReadableStreamDownloadCancel) |
| { |
| [WKWebsiteDataStore _allowWebsiteDataRecordsForAllOrigins]; |
| |
| // Start with a clean slate data store |
| [[WKWebsiteDataStore defaultDataStore] removeDataOfTypes:[WKWebsiteDataStore allWebsiteDataTypes] modifiedSince:[NSDate distantPast] completionHandler:^() { |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| done = false; |
| |
| auto dataStoreConfiguration = adoptNS([[_WKWebsiteDataStoreConfiguration alloc] init]); |
| [dataStoreConfiguration setServiceWorkerProcessTerminationDelayEnabled:NO]; |
| auto dataStore = adoptNS([[WKWebsiteDataStore alloc] _initWithConfiguration:dataStoreConfiguration.get()]); |
| |
| auto configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| setConfigurationInjectedBundlePath(configuration.get()); |
| configuration.get().websiteDataStore = dataStore.get(); |
| |
| auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSMakeRect(0, 0, 300, 300) configuration:configuration.get() addToWindow:YES]); |
| |
| auto navigationDelegate = adoptNS([TestNavigationDelegate new]); |
| [webView setNavigationDelegate:navigationDelegate.get()]; |
| |
| TestWebKitAPI::HTTPServer server({ |
| { "/"_s, { ServiceWorkerReadableStreamDownloadMain } }, |
| { "/sw.js"_s, { {{ "Content-Type"_s, "application/javascript"_s }}, ServiceWorkerReadableStreamDownloadJS } } |
| }, TestWebKitAPI::HTTPServer::Protocol::Http, nullptr, nullptr, 8091); |
| |
| [webView loadRequest:server.request()]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "Ready"); |
| |
| auto navigationDownloadDelegate = adoptNS([TestDownloadDelegate new]); |
| [webView setNavigationDelegate:navigationDownloadDelegate.get()]; |
| |
| navigationDownloadDelegate.get().navigationResponseDidBecomeDownload = ^(WKWebView *, WKNavigationResponse *, WKDownload *download) { |
| int64_t deferredWaitTime = 500 * NSEC_PER_MSEC; |
| dispatch_time_t when = dispatch_time(DISPATCH_TIME_NOW, deferredWaitTime); |
| dispatch_after(when, mainDispatchQueueSingleton(), ^{ |
| [download cancel:^(NSData*) { }]; |
| }); |
| }; |
| |
| for (size_t cptr = 0; cptr < 5; ++cptr) { |
| [webView evaluateJavaScript:@"doTest()" completionHandler: nil]; |
| EXPECT_WK_STREQ([webView _test_waitForAlert], "Cancelled"); |
| } |
| } |
| |
| #endif // WK_HAVE_C_SPI |