/* * Copyright (C) 2017-2021 Apple Inc. All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF * THE POSSIBILITY OF SUCH DAMAGE. */ #import "config.h" #if ENABLE(APPLICATION_MANIFEST) #import "PlatformUtilities.h" #import "Test.h" #import "TestCocoa.h" #import "TestNavigationDelegate.h" #import "TestWKWebView.h" #import #import #import #import #import namespace TestWebKitAPI { TEST(ApplicationManifest, Coding) { auto jsonString = @"{ \"name\": \"TestName\", \"short_name\": \"TestShortName\", \"description\": \"TestDescription\", \"scope\": \"https://test.com/app\", \"start_url\": \"https://test.com/app/index.html\", \"display\": \"minimal-ui\", \"theme_color\": \"red\" }"; RetainPtr<_WKApplicationManifest> manifest { [_WKApplicationManifest applicationManifestFromJSON:jsonString manifestURL:[NSURL URLWithString:@"https://test.com/manifest.json"] documentURL:[NSURL URLWithString:@"https://test.com/"]] }; NSData *data = [NSKeyedArchiver archivedDataWithRootObject:manifest.get() requiringSecureCoding:YES error:nullptr]; manifest = [NSKeyedUnarchiver unarchivedObjectOfClasses:[NSSet setWithObject:[_WKApplicationManifest class]] fromData:data error:nullptr]; EXPECT_TRUE([manifest isKindOfClass:[_WKApplicationManifest class]]); EXPECT_STREQ("TestName", manifest.get().name.UTF8String); EXPECT_STREQ("TestShortName", manifest.get().shortName.UTF8String); EXPECT_STREQ("TestDescription", manifest.get().applicationDescription.UTF8String); EXPECT_STREQ("https://test.com/app", manifest.get().scope.absoluteString.UTF8String); EXPECT_STREQ("https://test.com/app/index.html", manifest.get().startURL.absoluteString.UTF8String); EXPECT_EQ(_WKApplicationManifestDisplayModeMinimalUI, manifest.get().displayMode); auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); EXPECT_TRUE(CGColorEqualToColor(manifest.get().themeColor.CGColor, redColor.get())); } TEST(ApplicationManifest, Basic) { static bool done = false; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect]); [webView synchronouslyLoadHTMLString:@""]; [webView.get() _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { EXPECT_NULL(manifest); done = true; }]; Util::run(&done); done = false; [webView synchronouslyLoadHTMLString:@""]; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { EXPECT_NULL(manifest); done = true; }]; Util::run(&done); done = false; NSDictionary *manifestObject = @{ @"name": @"Test" }; [webView synchronouslyLoadHTMLString:[NSString stringWithFormat:@"", [[NSJSONSerialization dataWithJSONObject:manifestObject options:0 error:nil] base64EncodedStringWithOptions:0]]]; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { EXPECT_TRUE([manifest.name isEqualToString:@"Test"]); done = true; }]; Util::run(&done); done = false; manifestObject = @{ @"name": @"A Web Application", @"short_name": @"WebApp", @"description": @"Hello.", @"start_url": @"http://example.com/app/start", @"scope": @"http://example.com/app", @"theme_color": @"red", }; NSString *htmlString = [NSString stringWithFormat:@"", [[NSJSONSerialization dataWithJSONObject:manifestObject options:0 error:nil] base64EncodedStringWithOptions:0]]; [webView loadHTMLString:htmlString baseURL:[NSURL URLWithString:@"http://example.com/app/index"]]; [webView _test_waitForDidFinishNavigation]; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { EXPECT_TRUE([manifest.name isEqualToString:@"A Web Application"]); EXPECT_TRUE([manifest.shortName isEqualToString:@"WebApp"]); EXPECT_TRUE([manifest.applicationDescription isEqualToString:@"Hello."]); EXPECT_TRUE([manifest.startURL isEqual:[NSURL URLWithString:@"http://example.com/app/start"]]); EXPECT_TRUE([manifest.scope isEqual:[NSURL URLWithString:@"http://example.com/app"]]); auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); EXPECT_TRUE(CGColorEqualToColor(manifest.themeColor.CGColor, redColor.get())); done = true; }]; Util::run(&done); } TEST(ApplicationManifest, DisplayMode) { static bool done; NSDictionary *displayModesAndExpectedContent = @{ @"": @"(display-mode) (display-mode: browser)", @"browser": @"(display-mode) (display-mode: browser)", @"minimal-ui": @"(display-mode) (display-mode: minimal-ui)", @"standalone": @"(display-mode) (display-mode: standalone)", @"fullscreen": @"(display-mode) (display-mode: fullscreen)", }; NSURL *baseURL = [[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:@"TestWebKitAPI.resources"]; [displayModesAndExpectedContent enumerateKeysAndObjectsUsingBlock:^(NSString *displayMode, NSString *expectedPageContent, BOOL* stop) { @autoreleasepool { NSString *m2 = displayMode.length ? [NSString stringWithFormat:@"{\"display\": \"%@\"}", displayMode] : @"{}"; RetainPtr<_WKApplicationManifest> manifest = [_WKApplicationManifest applicationManifestFromJSON:m2 manifestURL:[baseURL URLByAppendingPathComponent:@"manifest.json"] documentURL:baseURL]; auto config = adoptNS([[WKWebViewConfiguration alloc] init]); config.get()._applicationManifest = manifest.get(); auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectZero configuration:config.get()]); [webView synchronouslyLoadTestPageNamed:@"display-mode"]; done = false; [webView _getContentsAsStringWithCompletionHandler:^(NSString *contents, NSError *error) { done = true; EXPECT_STREQ(expectedPageContent.UTF8String, contents.UTF8String); }]; Util::run(&done); [webView removeFromSuperview]; } }]; } TEST(ApplicationManifest, AlwaysFetch) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect]); NSDictionary *manifestObject = @{ @"theme_color": @"red" }; [webView synchronouslyLoadHTMLString:[NSString stringWithFormat:@"", [[NSJSONSerialization dataWithJSONObject:manifestObject options:0 error:nil] base64EncodedStringWithOptions:0]]]; { auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); while (!CGColorEqualToColor([webView themeColor].CGColor, redColor.get())) Util::sleep(1); } __block bool done = false; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); EXPECT_TRUE(CGColorEqualToColor(manifest.themeColor.CGColor, redColor.get())); done = true; }]; Util::run(&done); } TEST(ApplicationManifest, OnlyFirstManifest) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect]); NSDictionary *manifestObject1 = @{ @"theme_color": @"red" }; NSDictionary *manifestObject2 = @{ @"theme_color": @"blue" }; [webView synchronouslyLoadHTMLString:[NSString stringWithFormat:@"", [[NSJSONSerialization dataWithJSONObject:manifestObject1 options:0 error:nil] base64EncodedStringWithOptions:0], [[NSJSONSerialization dataWithJSONObject:manifestObject2 options:0 error:nil] base64EncodedStringWithOptions:0]]]; { auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); while (!CGColorEqualToColor([webView themeColor].CGColor, redColor.get())) Util::sleep(1); } __block bool done = false; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); EXPECT_TRUE(CGColorEqualToColor(manifest.themeColor.CGColor, redColor.get())); done = true; }]; Util::run(&done); } TEST(ApplicationManifest, NoManifest) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect]); [webView synchronouslyLoadHTMLString:@"Hello World"]; EXPECT_NULL([webView themeColor]); __block bool done = false; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { EXPECT_NULL(manifest); done = true; }]; Util::run(&done); } TEST(ApplicationManifest, MediaAttriute) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect]); NSDictionary *manifestObject1 = @{ @"theme_color": @"blue" }; NSDictionary *manifestObject2 = @{ @"theme_color": @"red" }; [webView synchronouslyLoadHTMLString:[NSString stringWithFormat:@"", [[NSJSONSerialization dataWithJSONObject:manifestObject1 options:0 error:nil] base64EncodedStringWithOptions:0], [[NSJSONSerialization dataWithJSONObject:manifestObject2 options:0 error:nil] base64EncodedStringWithOptions:0]]]; { auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); while (!CGColorEqualToColor([webView themeColor].CGColor, redColor.get())) Util::sleep(1); } __block bool done = false; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); EXPECT_TRUE(CGColorEqualToColor(manifest.themeColor.CGColor, redColor.get())); done = true; }]; Util::run(&done); } TEST(ApplicationManifest, DoesNotExist) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect]); [webView synchronouslyLoadHTMLString:@""]; EXPECT_NULL([webView themeColor]); __block bool done = false; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { EXPECT_NULL(manifest); done = true; }]; Util::run(&done); } TEST(ApplicationManifest, Blocked) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect]); NSDictionary *manifestObject = @{ @"theme_color": @"red" }; [webView synchronouslyLoadHTMLString:[NSString stringWithFormat:@"", [[NSJSONSerialization dataWithJSONObject:manifestObject options:0 error:nil] base64EncodedStringWithOptions:0]]]; EXPECT_NULL([webView themeColor]); __block bool done = false; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { EXPECT_NULL(manifest); done = true; }]; Util::run(&done); } TEST(ApplicationManifest, Icons) { static bool done = false; NSArray *expectedIcons = @[ @{ @"src": @"https://example.com/images/touch/homescreen32.png", @"sizes": @"32x32", @"type": @"image/png" }, @{ @"src": @"https://example.com/images/touch/homescreen48.png", @"sizes": @"48x48", @"type": @"image/png", @"purpose": @"monochrome maskable" }, @{ @"src": @"https://example.com/images/touch/homescreen128.jpg", @"sizes": @"96x96 128x128", @"type": @"image/jpg", @"purpose": @"monochrome" }]; auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:NSZeroRect]); NSDictionary *manifestObject = @{ @"name": @"A Web Application", @"short_name": @"WebApp", @"description": @"Hello.", @"start_url": @"http://example.com/app/start", @"scope": @"http://example.com/app", @"theme_color": @"red", @"icons": expectedIcons }; NSString *htmlString = [NSString stringWithFormat:@"", [[NSJSONSerialization dataWithJSONObject:manifestObject options:0 error:nil] base64EncodedStringWithOptions:0]]; [webView loadHTMLString:htmlString baseURL:[NSURL URLWithString:@"http://example.com/app/index"]]; [webView _test_waitForDidFinishNavigation]; [webView _getApplicationManifestWithCompletionHandler:^(_WKApplicationManifest *manifest) { EXPECT_TRUE([manifest.name isEqualToString:@"A Web Application"]); EXPECT_TRUE([manifest.shortName isEqualToString:@"WebApp"]); EXPECT_TRUE([manifest.applicationDescription isEqualToString:@"Hello."]); EXPECT_TRUE([manifest.startURL isEqual:[NSURL URLWithString:@"http://example.com/app/start"]]); EXPECT_TRUE([manifest.scope isEqual:[NSURL URLWithString:@"http://example.com/app"]]); auto sRGBColorSpace = adoptCF(CGColorSpaceCreateWithName(kCGColorSpaceSRGB)); auto redColor = adoptCF(CGColorCreate(sRGBColorSpace.get(), redColorComponents)); EXPECT_TRUE(CGColorEqualToColor(manifest.themeColor.CGColor, redColor.get())); size_t iconIndex = 0; for (_WKApplicationManifestIcon *icon in manifest.icons) { NSDictionary *expectedIcon = expectedIcons[iconIndex]; NSString *expectedURLString = [expectedIcon objectForKey:@"src"]; EXPECT_TRUE([icon.src isEqual:[NSURL URLWithString:expectedURLString]]); EXPECT_TRUE([icon.type isEqual:[expectedIcon objectForKey:@"type"]]); switch (iconIndex) { case 0: EXPECT_EQ(icon.sizes.count, 1ul); EXPECT_TRUE([icon.sizes[0] isEqual:[expectedIcon objectForKey:@"sizes"]]); EXPECT_EQ(icon.purposes.count, 1ul); EXPECT_EQ(icon.purposes[0].unsignedLongValue, 1ul); break; case 1: EXPECT_EQ(icon.sizes.count, 1ul); EXPECT_TRUE([icon.sizes[0] isEqual:[expectedIcon objectForKey:@"sizes"]]); EXPECT_EQ(icon.purposes.count, 2ul); EXPECT_EQ(icon.purposes[0].unsignedLongValue, 2ul); EXPECT_EQ(icon.purposes[1].unsignedLongValue, 4ul); break; case 2: EXPECT_EQ(icon.sizes.count, 2ul); EXPECT_TRUE([icon.sizes[0] isEqual:@"96x96"]); EXPECT_TRUE([icon.sizes[1] isEqual:@"128x128"]); EXPECT_EQ(icon.purposes.count, 1ul); EXPECT_EQ(icon.purposes[0].unsignedLongValue, 2ul); break; } ++iconIndex; } done = true; }]; Util::run(&done); } TEST(ApplicationManifest, IconCoding) { static constexpr auto testURL = "https://example.com/images/touch/homescreen128.jpg"_s; WebCore::ApplicationManifest::Icon icon = { URL { testURL }, makeVector(@[@"96x96", @"128x128"]), "image/jpg"_s, { WebCore::ApplicationManifest::Icon::Purpose::Monochrome, WebCore::ApplicationManifest::Icon::Purpose::Maskable } }; IGNORE_WARNINGS_BEGIN("objc-method-access") auto manifestIcon = adoptNS([[_WKApplicationManifestIcon alloc] initWithCoreIcon:&icon]); IGNORE_WARNINGS_END NSError *error = nil; NSData *archiveData = [NSKeyedArchiver archivedDataWithRootObject:manifestIcon.get() requiringSecureCoding:YES error:&error]; EXPECT_NULL(error); _WKApplicationManifestIcon *decodedIcon = [NSKeyedUnarchiver unarchivedObjectOfClass:[_WKApplicationManifestIcon class] fromData:archiveData error:&error]; EXPECT_NULL(error); EXPECT_TRUE([decodedIcon isKindOfClass:[_WKApplicationManifestIcon class]]); EXPECT_STREQ(testURL, decodedIcon.src.absoluteString.UTF8String); EXPECT_TRUE([decodedIcon.sizes[0] isEqual:@"96x96"]); EXPECT_TRUE([decodedIcon.sizes[1] isEqual:@"128x128"]); EXPECT_TRUE([decodedIcon.type isEqual:@"image/jpg"]); EXPECT_EQ(decodedIcon.purposes.count, 2ul); EXPECT_EQ(decodedIcon.purposes[0].unsignedLongValue, 2ul); EXPECT_EQ(decodedIcon.purposes[1].unsignedLongValue, 4ul); } } // namespace TestWebKitAPI #endif // ENABLE(APPLICATION_MANIFEST)