blob: b9890be825d9749b4f38642ffeeee41d9d57d7d7 [file] [log] [blame]
/*
* 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