| /* |
| * Copyright (C) 2021 Apple Inc. All rights reserved. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions |
| * are met: |
| * 1. Redistributions of source code must retain the above copyright |
| * notice, this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright |
| * notice, this list of conditions and the following disclaimer in the |
| * documentation and/or other materials provided with the distribution. |
| * |
| * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS'' |
| * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, |
| * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR |
| * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS |
| * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR |
| * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF |
| * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS |
| * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN |
| * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) |
| * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF |
| * THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #import "config.h" |
| #import "Test.h" |
| |
| #import "PlatformUtilities.h" |
| #import "TestNavigationDelegate.h" |
| #import "TestProtocol.h" |
| #import "TestWKWebView.h" |
| #import "WKWebViewConfigurationExtras.h" |
| #import <WebKit/WKPreferencesPrivate.h> |
| #import <WebKit/WKUIDelegatePrivate.h> |
| #import <WebKit/WKWebViewConfigurationPrivate.h> |
| #import <WebKit/WKWebpagePreferencesPrivate.h> |
| #import <WebKit/_WKModalContainerInfo.h> |
| #import <objc/runtime.h> |
| #import <wtf/FastMalloc.h> |
| #import <wtf/SetForScope.h> |
| |
| @interface NSBundle (TestSupport) |
| - (NSURL *)swizzled_URLForResource:(NSString *)name withExtension:(NSString *)extension; |
| @end |
| |
| @implementation NSBundle (TestSupport) |
| |
| - (NSURL *)swizzled_URLForResource:(NSString *)name withExtension:(NSString *)extension |
| { |
| if ([name isEqualToString:@"ModalContainerControls"] && [extension isEqualToString:@"mlmodelc"]) { |
| // Override the default CoreML model with a smaller dataset that's limited to testing purposes. |
| return [NSBundle.mainBundle URLForResource:@"TestModalContainerControls" withExtension:@"mlmodelc" subdirectory:@"TestWebKitAPI.resources"]; |
| } |
| // Call through to the original method implementation if we're not specifically requesting ModalContainerControls.mlmodelc. |
| return [self swizzled_URLForResource:name withExtension:extension]; |
| } |
| |
| @end |
| |
| namespace TestWebKitAPI { |
| |
| class ClassifierModelSwizzler { |
| WTF_MAKE_FAST_ALLOCATED; |
| public: |
| ClassifierModelSwizzler() |
| : m_originalMethod(class_getInstanceMethod(NSBundle.class, @selector(URLForResource:withExtension:))) |
| , m_swizzledMethod(class_getInstanceMethod(NSBundle.class, @selector(swizzled_URLForResource:withExtension:))) |
| { |
| m_originalImplementation = method_getImplementation(m_originalMethod); |
| m_swizzledImplementation = method_getImplementation(m_swizzledMethod); |
| class_replaceMethod(NSBundle.class, @selector(swizzled_URLForResource:withExtension:), m_originalImplementation, method_getTypeEncoding(m_originalMethod)); |
| class_replaceMethod(NSBundle.class, @selector(URLForResource:withExtension:), m_swizzledImplementation, method_getTypeEncoding(m_swizzledMethod)); |
| } |
| |
| ~ClassifierModelSwizzler() |
| { |
| class_replaceMethod(NSBundle.class, @selector(swizzled_URLForResource:withExtension:), m_swizzledImplementation, method_getTypeEncoding(m_originalMethod)); |
| class_replaceMethod(NSBundle.class, @selector(URLForResource:withExtension:), m_originalImplementation, method_getTypeEncoding(m_swizzledMethod)); |
| } |
| |
| private: |
| Method m_originalMethod; |
| Method m_swizzledMethod; |
| IMP m_originalImplementation; |
| IMP m_swizzledImplementation; |
| }; |
| |
| } // namespace TestWebKitAPI |
| |
| @interface ModalContainerWebView : TestWKWebView <WKUIDelegatePrivate> |
| @property (nonatomic) _WKModalContainerDecision decision; |
| @property (nonatomic, readonly) _WKModalContainerInfo *lastModalContainerInfo; |
| @end |
| |
| @implementation ModalContainerWebView { |
| std::unique_ptr<TestWebKitAPI::ClassifierModelSwizzler> _classifierModelSwizzler; |
| RetainPtr<TestNavigationDelegate> _navigationDelegate; |
| RetainPtr<_WKModalContainerInfo> _lastModalContainerInfo; |
| bool _doneWaitingForDecisionHandler; |
| } |
| |
| - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration |
| { |
| if (!(self = [super initWithFrame:frame configuration:configuration])) |
| return nil; |
| |
| static std::once_flag onceFlag; |
| std::call_once(onceFlag, [] { |
| [TestProtocol registerWithScheme:@"http"]; |
| }); |
| |
| _decision = _WKModalContainerDecisionShow; |
| _classifierModelSwizzler = makeUnique<TestWebKitAPI::ClassifierModelSwizzler>(); |
| _navigationDelegate = adoptNS([[TestNavigationDelegate alloc] init]); |
| _doneWaitingForDecisionHandler = true; |
| [self setNavigationDelegate:_navigationDelegate.get()]; |
| [self setUIDelegate:self]; |
| return self; |
| } |
| |
| - (void)loadBundlePage:(NSString *)page andDecidePolicy:(_WKModalContainerDecision)decision |
| { |
| SetForScope decisionScope { _decision, decision }; |
| _doneWaitingForDecisionHandler = false; |
| |
| [self loadBundlePage:page]; |
| |
| TestWebKitAPI::Util::run(&_doneWaitingForDecisionHandler); |
| [self waitForNextPresentationUpdate]; |
| } |
| |
| - (void)evaluate:(NSString *)script andDecidePolicy:(_WKModalContainerDecision)decision |
| { |
| SetForScope decisionScope { _decision, decision }; |
| _doneWaitingForDecisionHandler = false; |
| |
| [self objectByEvaluatingJavaScript:script]; |
| |
| TestWebKitAPI::Util::run(&_doneWaitingForDecisionHandler); |
| [self waitForNextPresentationUpdate]; |
| } |
| |
| - (void)loadBundlePage:(NSString *)page |
| { |
| NSURL *bundleURL = [NSBundle.mainBundle URLForResource:page withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]; |
| NSURLRequest *fakeRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]]; |
| [self loadSimulatedRequest:fakeRequest responseHTMLString:[NSString stringWithContentsOfURL:bundleURL]]; |
| |
| auto preferences = adoptNS([[WKWebpagePreferences alloc] init]); |
| [preferences _setModalContainerObservationPolicy:_WKWebsiteModalContainerObservationPolicyPrompt]; |
| [_navigationDelegate waitForDidFinishNavigationWithPreferences:preferences.get()]; |
| } |
| |
| - (void)loadHTML:(NSString *)html |
| { |
| NSURLRequest *fakeRequest = [NSURLRequest requestWithURL:[NSURL URLWithString:@"http://webkit.org"]]; |
| [self loadSimulatedRequest:fakeRequest responseHTMLString:html]; |
| |
| auto preferences = adoptNS([[WKWebpagePreferences alloc] init]); |
| [preferences _setModalContainerObservationPolicy:_WKWebsiteModalContainerObservationPolicyPrompt]; |
| [_navigationDelegate waitForDidFinishNavigationWithPreferences:preferences.get()]; |
| } |
| |
| - (void)waitForText:(NSString *)text |
| { |
| TestWebKitAPI::Util::waitForConditionWithLogging([&]() -> bool { |
| return [self.contentsAsString containsString:text]; |
| }, 3, @"Timed out waiting for '%@'", text); |
| } |
| |
| - (void)_webView:(WKWebView *)webView decidePolicyForModalContainer:(_WKModalContainerInfo *)containerInfo decisionHandler:(void (^)(_WKModalContainerDecision))handler |
| { |
| handler(_decision); |
| _lastModalContainerInfo = containerInfo; |
| _doneWaitingForDecisionHandler = true; |
| } |
| |
| - (_WKModalContainerInfo *)lastModalContainerInfo |
| { |
| return _lastModalContainerInfo.get(); |
| } |
| |
| @end |
| |
| namespace TestWebKitAPI { |
| |
| static RetainPtr<ModalContainerWebView> createModalContainerWebView() |
| { |
| auto configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"WebProcessPlugInWithInternals" configureJSCForTesting:YES]; |
| return adoptNS([[ModalContainerWebView alloc] initWithFrame:CGRectMake(0, 0, 400, 400) configuration:configuration]); |
| } |
| |
| constexpr auto allControlTypes = _WKModalContainerControlTypeNeutral | _WKModalContainerControlTypePositive | _WKModalContainerControlTypeNegative; |
| |
| TEST(ModalContainerObservation, HideAndAllowModalContainer) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView loadBundlePage:@"modal-container" andDecidePolicy:_WKModalContainerDecisionHideAndAllow]; |
| NSString *visibleText = [webView contentsAsString]; |
| EXPECT_TRUE([visibleText containsString:@"Clicked on \"Yes\""]); |
| EXPECT_FALSE([visibleText containsString:@"Hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, allControlTypes); |
| } |
| |
| TEST(ModalContainerObservation, HideAndDisallowModalContainer) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView loadBundlePage:@"modal-container" andDecidePolicy:_WKModalContainerDecisionHideAndDisallow]; |
| NSString *visibleText = [webView contentsAsString]; |
| EXPECT_TRUE([visibleText containsString:@"Clicked on \"No\""]); |
| EXPECT_FALSE([visibleText containsString:@"Hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, allControlTypes); |
| } |
| |
| TEST(ModalContainerObservation, HideAndIgnoreModalContainer) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView loadBundlePage:@"modal-container" andDecidePolicy:_WKModalContainerDecisionHideAndIgnore]; |
| NSString *visibleText = [webView contentsAsString]; |
| EXPECT_FALSE([visibleText containsString:@"Clicked on"]); |
| EXPECT_FALSE([visibleText containsString:@"Hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, allControlTypes); |
| } |
| |
| TEST(ModalContainerObservation, ShowModalContainer) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView loadBundlePage:@"modal-container" andDecidePolicy:_WKModalContainerDecisionShow]; |
| NSString *visibleText = [webView contentsAsString]; |
| EXPECT_FALSE([visibleText containsString:@"Clicked on"]); |
| EXPECT_TRUE([visibleText containsString:@"Hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, allControlTypes); |
| } |
| |
| TEST(ModalContainerObservation, ClassifyMultiplySymbol) |
| { |
| auto webView = createModalContainerWebView(); |
| auto runTest = [&] (NSString *symbol) { |
| [webView loadBundlePage:@"modal-container-custom"]; |
| NSString *scriptToEvaluate = [NSString stringWithFormat:@"show(`<p>Hello world</p><button>%@</button>`)", symbol]; |
| [webView evaluate:scriptToEvaluate andDecidePolicy:_WKModalContainerDecisionHideAndIgnore]; |
| |
| EXPECT_FALSE([[webView contentsAsString] containsString:@"Hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, _WKModalContainerControlTypeNeutral); |
| }; |
| runTest(@"✕"); |
| runTest(@"⨯"); |
| } |
| |
| TEST(ModalContainerObservation, DetectSearchTermInBoldTag) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView loadBundlePage:@"modal-container-custom"]; |
| [webView evaluate:@"show(`<b>Hello world</b><a href='#'>Yes</a><a href='#'>No</a>`)" andDecidePolicy:_WKModalContainerDecisionHideAndIgnore]; |
| |
| EXPECT_FALSE([[webView contentsAsString] containsString:@"Hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, _WKModalContainerControlTypePositive | _WKModalContainerControlTypeNegative); |
| } |
| |
| TEST(ModalContainerObservation, HideUserInteractionBlockingElementAndMakeDocumentScrollable) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView loadBundlePage:@"modal-container-with-overlay" andDecidePolicy:_WKModalContainerDecisionHideAndIgnore]; |
| |
| EXPECT_FALSE([[webView contentsAsString] containsString:@"Hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, _WKModalContainerControlTypePositive); |
| |
| NSString *hitTestedText = [webView stringByEvaluatingJavaScript:@"document.elementFromPoint(50, 50).textContent"]; |
| EXPECT_TRUE([hitTestedText containsString:@"Lorem"]); |
| EXPECT_WK_STREQ("auto", [webView stringByEvaluatingJavaScript:@"getComputedStyle(document.documentElement).overflowY"]); |
| EXPECT_WK_STREQ("auto", [webView stringByEvaluatingJavaScript:@"getComputedStyle(document.body).overflowY"]); |
| } |
| |
| TEST(ModalContainerObservation, IgnoreFixedDocumentElement) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView setDecision:_WKModalContainerDecisionHideAndIgnore]; |
| [webView loadHTML:@("<head>" |
| "<script>internals.overrideModalContainerSearchTermForTesting('foo')</script>" |
| "<style>html { position: fixed; width: 100%; height: 100%; top: 0; left: 0; }</style>" |
| "</head><body><h1>foo bar</h1></body>")]; |
| |
| EXPECT_TRUE([[webView contentsAsString] containsString:@"foo bar"]); |
| EXPECT_NULL([webView lastModalContainerInfo]); |
| } |
| |
| TEST(ModalContainerObservation, IgnoreNavigationElements) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView setDecision:_WKModalContainerDecisionHideAndIgnore]; |
| [webView loadBundlePage:@"modal-container-custom"]; |
| [webView objectByEvaluatingJavaScript:@"show(`<nav>hello world 1</nav><div role='navigation'>hello world 2</div>`)"]; |
| [webView waitForNextPresentationUpdate]; |
| |
| EXPECT_TRUE([[webView contentsAsString] containsString:@"hello world 1"]); |
| EXPECT_TRUE([[webView contentsAsString] containsString:@"hello world 2"]); |
| EXPECT_NULL([webView lastModalContainerInfo]); |
| } |
| |
| TEST(ModalContainerObservation, ShowModalContainerAfterFalsePositive) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView setDecision:_WKModalContainerDecisionHideAndIgnore]; |
| [webView loadBundlePage:@"modal-container-custom"]; |
| [webView objectByEvaluatingJavaScript:@"show(`<div>hello world</div><a href='https://apple.com'>Click here</a>`)"]; |
| [webView waitForNextPresentationUpdate]; |
| [webView waitForText:@"hello world"]; |
| EXPECT_NULL([webView lastModalContainerInfo]); |
| } |
| |
| TEST(ModalContainerObservation, ModalContainerInSubframe) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView setDecision:_WKModalContainerDecisionHideAndIgnore]; |
| [webView loadBundlePage:@"modal-container-custom"]; |
| [webView evaluate:@"show(`<p>subframe test</p><iframe srcdoc='<h2>hello world</h2><button>YES</button>'></iframe>`)" andDecidePolicy:_WKModalContainerDecisionHideAndIgnore]; |
| EXPECT_FALSE([[webView contentsAsString] containsString:@"subframe test"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, _WKModalContainerControlTypePositive); |
| } |
| |
| TEST(ModalContainerObservation, DetectModalContainerAfterSettingText) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView loadBundlePage:@"modal-container-custom"]; |
| [webView objectByEvaluatingJavaScript:@"show(`<div id='content'></div>`)"]; |
| [webView waitForNextPresentationUpdate]; |
| [webView evaluate:@"document.getElementById('content').innerHTML = `hello world <a href='#'>no</a>`" andDecidePolicy:_WKModalContainerDecisionHideAndIgnore]; |
| EXPECT_FALSE([[webView contentsAsString] containsString:@"hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, _WKModalContainerControlTypeNegative); |
| } |
| |
| TEST(ModalContainerObservation, DetectControlsWithEventListenersOnModalContainer) |
| { |
| auto webView = createModalContainerWebView(); |
| [webView loadBundlePage:@"modal-container-custom"]; |
| auto script = @"showWithEventListener(`<div>Hello world <span style='cursor: pointer;'>yes</span></div>`, 'click', () => window.testPassed = true)"; |
| [webView evaluate:script andDecidePolicy:_WKModalContainerDecisionHideAndAllow]; |
| [webView waitForNextPresentationUpdate]; |
| EXPECT_FALSE([[webView contentsAsString] containsString:@"Hello world"]); |
| EXPECT_EQ([webView lastModalContainerInfo].availableTypes, _WKModalContainerControlTypePositive); |
| EXPECT_TRUE([[webView objectByEvaluatingJavaScript:@"window.testPassed"] boolValue]); |
| } |
| |
| } // namespace TestWebKitAPI |