| /* |
| * Copyright (C) 2025-2026 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 "HTTPServer.h" |
| #import "InstanceMethodSwizzler.h" |
| #import "JSHandlePlugInProtocol.h" |
| #import "PlatformUtilities.h" |
| #import "SafeBrowsingSPI.h" |
| #import "Test.h" |
| #import "TestNavigationDelegate.h" |
| #import "TestWKWebView.h" |
| #import "Utilities.h" |
| #import "WKWebViewConfigurationExtras.h" |
| #import <WebKit/WKContentWorldPrivate.h> |
| #import <WebKit/WKFrameInfoPrivate.h> |
| #import <WebKit/WKPreferencesPrivate.h> |
| #import <WebKit/WKWebViewConfigurationPrivate.h> |
| #import <WebKit/WKWebViewPrivate.h> |
| #import <WebKit/_WKContentWorldConfiguration.h> |
| #import <WebKit/_WKFrameTreeNode.h> |
| #import <WebKit/_WKJSHandle.h> |
| #import <WebKit/_WKRemoteObjectInterface.h> |
| #import <WebKit/_WKRemoteObjectRegistry.h> |
| #import <WebKit/_WKTextExtraction.h> |
| #import <pal/spi/cocoa/NSKeyedUnarchiverSPI.h> |
| #import <wtf/SoftLinking.h> |
| #import <wtf/WorkQueue.h> |
| #import <wtf/cocoa/TypeCastsCocoa.h> |
| #import <wtf/text/MakeString.h> |
| |
| SOFT_LINK_PRIVATE_FRAMEWORK(SafariSafeBrowsing); |
| SOFT_LINK_CLASS(SafariSafeBrowsing, SSBLookupContext); |
| |
| @interface WKWebView (TextExtractionTests) |
| - (NSString *)synchronouslyGetDebugText:(_WKTextExtractionConfiguration *)configuration; |
| - (_WKTextExtractionResult *)synchronouslyExtractDebugTextResult:(_WKTextExtractionConfiguration *)configuration; |
| - (_WKTextExtractionInteractionResult *)synchronouslyPerformInteraction:(_WKTextExtractionInteraction *)interaction; |
| - (NSData *)synchronouslyGetSelectorPathDataForNode:(_WKJSHandle *)node; |
| - (_WKJSHandle *)synchronouslyGetNodeForSelectorPathData:(NSData *)data; |
| @end |
| |
| @interface _WKTextExtractionInteraction (TextExtractionTests) |
| - (NSString *)debugDescriptionInWebView:(WKWebView *)webView error:(NSError **)outError; |
| @end |
| |
| @interface _WKTextExtractionResult (TextExtractionTests) |
| - (_WKJSHandle *)jsHandleForNodeIdentifier:(NSString *)nodeIdentifier searchText:(NSString *)searchText; |
| @end |
| |
| @implementation WKWebView (TextExtractionTests) |
| |
| - (NSString *)synchronouslyGetDebugText:(_WKTextExtractionConfiguration *)configuration |
| { |
| return [[self synchronouslyExtractDebugTextResult:configuration] textContent]; |
| } |
| |
| - (_WKTextExtractionResult *)synchronouslyExtractDebugTextResult:(_WKTextExtractionConfiguration *)configuration |
| { |
| RetainPtr configurationToUse = configuration; |
| if (!configurationToUse) |
| configurationToUse = adoptNS([_WKTextExtractionConfiguration new]); |
| |
| __block bool done = false; |
| __block RetainPtr<_WKTextExtractionResult> result; |
| [self _extractDebugTextWithConfiguration:configurationToUse.get() completionHandler:^(_WKTextExtractionResult *extractionResult) { |
| result = extractionResult; |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| return result.autorelease(); |
| } |
| |
| - (_WKTextExtractionInteractionResult *)synchronouslyPerformInteraction:(_WKTextExtractionInteraction *)interaction |
| { |
| __block bool done = false; |
| __block RetainPtr<_WKTextExtractionInteractionResult> result; |
| [self _performInteraction:interaction completionHandler:^(_WKTextExtractionInteractionResult *theResult) { |
| result = theResult; |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| return result.autorelease(); |
| } |
| |
| - (NSData *)synchronouslyGetSelectorPathDataForNode:(_WKJSHandle *)node |
| { |
| __block bool done = false; |
| __block RetainPtr<NSData> result; |
| [self _getSelectorPathDataForNode:node completionHandler:^(NSData *data) { |
| result = data; |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| return result.autorelease(); |
| } |
| |
| - (_WKJSHandle *)synchronouslyGetNodeForSelectorPathData:(NSData *)data |
| { |
| __block bool done = false; |
| __block RetainPtr<_WKJSHandle> result; |
| [self _getNodeForSelectorPathData:data completionHandler:^(_WKJSHandle *handle) { |
| result = handle; |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| return result.autorelease(); |
| } |
| |
| @end |
| |
| @implementation _WKTextExtractionInteraction (TextExtractionTests) |
| |
| - (NSString *)debugDescriptionInWebView:(WKWebView *)webView error:(NSError **)outError |
| { |
| __block bool done = false; |
| __block RetainPtr<NSString> result; |
| __block RetainPtr<NSError> error; |
| [self debugDescriptionInWebView:webView completionHandler:^(NSString *description, NSError *theError) { |
| result = description; |
| error = theError; |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| if (outError) |
| *outError = error.autorelease(); |
| return result.autorelease(); |
| } |
| |
| @end |
| |
| @implementation _WKTextExtractionResult (TextExtractionTests) |
| |
| - (_WKJSHandle *)jsHandleForNodeIdentifier:(NSString *)nodeIdentifier searchText:(NSString *)searchText |
| { |
| __block bool done = false; |
| __block RetainPtr<_WKJSHandle> result; |
| [self requestJSHandleForNodeIdentifier:nodeIdentifier searchText:searchText completionHandler:^(_WKJSHandle *handle) { |
| result = handle; |
| done = true; |
| }]; |
| TestWebKitAPI::Util::run(&done); |
| return result.autorelease(); |
| } |
| |
| @end |
| |
| @interface JSHandleReceiver : NSObject<JSHandlePlugInProtocol> |
| @property (nonatomic, copy) void (^dictionaryReceiver)(NSDictionary *); |
| @end |
| @implementation JSHandleReceiver |
| - (void)receiveDictionaryFromWebProcess:(NSDictionary *)dictionary |
| { |
| _dictionaryReceiver(dictionary); |
| } |
| @end |
| |
| namespace TestWebKitAPI { |
| |
| static NSString *extractNodeIdentifier(NSString *debugText, NSString *searchText) |
| { |
| for (NSString *line in [debugText componentsSeparatedByString:@"\n"]) { |
| if (![line containsString:searchText]) |
| continue; |
| |
| RetainPtr regex = [NSRegularExpression regularExpressionWithPattern:@"uid=(((?:\\d+_)+)?\\d+)" options:0 error:nil]; |
| RetainPtr match = [regex firstMatchInString:line options:0 range:NSMakeRange(0, line.length)]; |
| if (!match) |
| continue; |
| |
| NSRange identifierRange = [match rangeAtIndex:1]; |
| return [line substringWithRange:identifierRange]; |
| } |
| |
| return nil; |
| } |
| |
| #if PLATFORM(MAC) |
| |
| TEST(TextExtractionTests, SelectPopupMenu) |
| { |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]); |
| |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr debugText = [webView synchronouslyGetDebugText:nil]; |
| RetainPtr click = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionClick]); |
| [click setNodeIdentifier:extractNodeIdentifier(debugText.get(), @"menu")]; |
| |
| __block bool doneSelectingOption = false; |
| __block RetainPtr<NSString> debugTextAfterClickingSelect; |
| [webView _performInteraction:click.get() completionHandler:^(_WKTextExtractionInteractionResult *clickResult) { |
| EXPECT_FALSE(clickResult.error); |
| EXPECT_TRUE([[webView synchronouslyGetDebugText:nil] containsString:@"nativePopupMenu"]); |
| |
| RetainPtr selectOption = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionSelectMenuItem]); |
| [selectOption setText:@"Three"]; |
| [webView _performInteraction:selectOption.get() completionHandler:^(_WKTextExtractionInteractionResult *selectOptionResult) { |
| EXPECT_FALSE(selectOptionResult.error); |
| doneSelectingOption = true; |
| }]; |
| }]; |
| |
| Util::run(&doneSelectingOption); |
| EXPECT_WK_STREQ("Three", [webView stringByEvaluatingJavaScript:@"select.value"]); |
| } |
| |
| #endif // PLATFORM(MAC) |
| |
| TEST(TextExtractionTests, InteractionDebugDescription) |
| { |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]); |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr debugText = [webView synchronouslyGetDebugText:nil]; |
| RetainPtr testButtonID = extractNodeIdentifier(debugText.get(), @"Test"); |
| RetainPtr emailID = extractNodeIdentifier(debugText.get(), @"email"); |
| RetainPtr composeID = extractNodeIdentifier(debugText.get(), @"Compose"); |
| RetainPtr selectID = extractNodeIdentifier(debugText.get(), @"select"); |
| |
| #if ENABLE(TEXT_EXTRACTION_FILTER) |
| EXPECT_FALSE([debugText containsString:@"crazy ones"]); |
| #endif |
| |
| NSError *error = nil; |
| NSString *description = nil; |
| { |
| RetainPtr interaction = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionClick]); |
| |
| [interaction setNodeIdentifier:testButtonID.get()]; |
| description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| EXPECT_WK_STREQ("Click on button labeled “Click Me”, with rendered text “Test”", description); |
| EXPECT_NULL(error); |
| |
| [interaction setNodeIdentifier:emailID.get()]; |
| description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| EXPECT_WK_STREQ("Click on input of type email with placeholder “Recipient address”", description); |
| EXPECT_NULL(error); |
| |
| [interaction setNodeIdentifier:composeID.get()]; |
| description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| EXPECT_WK_STREQ("Click on editable div labeled “Compose a new message”, with rendered text “Subject 'The quick brown fox jumped over the lazy dog'”, containing child labeled “Heading”", description); |
| EXPECT_NULL(error); |
| } |
| { |
| RetainPtr interaction = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionTextInput]); |
| |
| [interaction setNodeIdentifier:emailID.get()]; |
| [interaction setText:@"[email protected]"]; |
| [interaction setReplaceAll:YES]; |
| description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| EXPECT_WK_STREQ("Enter text “[email protected]” into input of type email with placeholder “Recipient address”, replacing any existing content", description); |
| EXPECT_NULL(error); |
| |
| [interaction setNodeIdentifier:composeID.get()]; |
| [interaction setText:@"«Testing»"]; |
| [interaction setReplaceAll:NO]; |
| description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| EXPECT_WK_STREQ("Enter text “'Testing'” into editable div labeled “Compose a new message”, with rendered text “Subject 'The quick brown fox jumped over the lazy dog'”, containing child labeled “Heading”", description); |
| EXPECT_NULL(error); |
| } |
| { |
| RetainPtr interaction = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionSelectMenuItem]); |
| [interaction setNodeIdentifier:selectID.get()]; |
| [interaction setText:@"Three"]; |
| description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| EXPECT_WK_STREQ("Select menu item “Three” in select with role “menu”", description); |
| EXPECT_NULL(error); |
| } |
| { |
| RetainPtr interaction = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionClick]); |
| auto clickLocation = [webView elementMidpointFromSelector:@"#test-button"]; |
| [interaction setLocation:clickLocation]; |
| description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| RetainPtr expectedString = [NSString stringWithFormat:@"Click at coordinates (%.0f, %.0f) on child node of button labeled “Click Me”, with rendered text “Test”", clickLocation.x, clickLocation.y]; |
| EXPECT_WK_STREQ(expectedString.get(), description); |
| EXPECT_NULL(error); |
| } |
| } |
| |
| TEST(TextExtractionTests, TargetNodeAndClientAttributes) |
| { |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| [webView evaluateJavaScript:@"getSelection().selectAllChildren(document.querySelector('h3[aria-label=\"Heading\"]'))" completionHandler:nil]; |
| |
| RetainPtr world = [WKContentWorld _worldWithConfiguration:^{ |
| RetainPtr configuration = adoptNS([_WKContentWorldConfiguration new]); |
| [configuration setAllowJSHandleCreation:YES]; |
| return configuration.autorelease(); |
| }()]; |
| |
| RetainPtr editorHandle = [webView querySelector:@"div[contenteditable]" frame:nil world:world.get()]; |
| RetainPtr headingHandle = [webView querySelector:@"h3[aria-label='Heading']" frame:nil world:world.get()]; |
| RetainPtr debugText = [webView synchronouslyGetDebugText:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setTargetNode:editorHandle.get()]; |
| [configuration addClientAttribute:@"extra-data-1" value:@"abc" forNode:editorHandle.get()]; |
| [configuration addClientAttribute:@"extra-data-1" value:@"123" forNode:headingHandle.get()]; |
| [configuration addClientAttribute:@"extra-data-2" value:@"xyz" forNode:headingHandle.get()]; |
| return configuration.autorelease(); |
| }()]; |
| |
| EXPECT_TRUE([debugText containsString:@"Compose a new message"]); |
| EXPECT_TRUE([debugText containsString:@"aria-label='Compose a new message',extra-data-1='abc'"]); |
| EXPECT_TRUE([debugText containsString:@"aria-label='Heading',extra-data-1='123',extra-data-2='xyz'"]); |
| EXPECT_TRUE([debugText containsString:@"Subject"]); |
| EXPECT_TRUE([debugText containsString:@"The quick brown fox jumped over the lazy dog"]); |
| EXPECT_FALSE([debugText containsString:@"select,"]); |
| EXPECT_FALSE([debugText containsString:@"Click Me"]); |
| EXPECT_FALSE([debugText containsString:@"Recipient address"]); |
| } |
| |
| TEST(TextExtractionTests, ReplacementStrings) |
| { |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr debugTextWithoutReplacements = [webView synchronouslyGetDebugText:nil]; |
| EXPECT_TRUE([debugTextWithoutReplacements containsString:@"The quick brown fox jumped over the lazy dog"]); |
| |
| RetainPtr debugTextWithReplacements = [webView synchronouslyGetDebugText:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setReplacementStrings:@{ |
| @"fox": @"cat", |
| @"dog": @"mouse", |
| @"lazy": @"" |
| }]; |
| return configuration.autorelease(); |
| }()]; |
| |
| EXPECT_FALSE([debugTextWithReplacements containsString:@"fox"]); |
| EXPECT_FALSE([debugTextWithReplacements containsString:@"dog"]); |
| EXPECT_FALSE([debugTextWithReplacements containsString:@"lazy"]); |
| EXPECT_TRUE([debugTextWithReplacements containsString:@"The quick brown cat jumped over the mouse"]); |
| } |
| |
| TEST(TextExtractionTests, VisibleTextOnly) |
| { |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr debugText = [webView synchronouslyGetDebugText:[_WKTextExtractionConfiguration configurationForVisibleTextOnly]]; |
| |
| EXPECT_TRUE([debugText containsString:@"Test"]); |
| EXPECT_TRUE([debugText containsString:@"foo"]); |
| EXPECT_TRUE([debugText containsString:@"Subject"]); |
| EXPECT_TRUE([debugText containsString:@"“The quick brown fox jumped over the lazy dog”"]); |
| EXPECT_TRUE([debugText containsString:@"0"]); |
| #if ENABLE(TEXT_EXTRACTION_FILTER) |
| EXPECT_FALSE([debugText containsString:@"Here’s to the crazy ones"]); |
| EXPECT_FALSE([debugText containsString:@"The round pegs in the square holes"]); |
| EXPECT_FALSE([debugText containsString:@"The ones who see things differently"]); |
| EXPECT_FALSE([debugText containsString:@"And they have no respect for the status quo"]); |
| EXPECT_FALSE([debugText containsString:@"They push the human race forward"]); |
| EXPECT_FALSE([debugText containsString:@"Because the people who are crazy"]); |
| #endif // ENABLE(TEXT_EXTRACTION_FILTER) |
| } |
| |
| TEST(TextExtractionTests, FilterOptions) |
| { |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| auto extractTextWithFilterOptions = [webView](_WKTextExtractionFilterOptions options) { |
| return [webView synchronouslyExtractDebugTextResult:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setFilterOptions:options]; |
| return configuration.autorelease(); |
| }()]; |
| }; |
| |
| { |
| RetainPtr result = extractTextWithFilterOptions(_WKTextExtractionFilterNone); |
| EXPECT_TRUE([[result textContent] containsString:@"“The quick brown fox jumped over the lazy dog”"]); |
| EXPECT_TRUE([[result textContent] containsString:@"Here’s to the crazy ones"]); |
| EXPECT_FALSE([result filteredOutAnyText]); |
| } |
| { |
| RetainPtr result = extractTextWithFilterOptions(_WKTextExtractionFilterTextRecognition); |
| EXPECT_TRUE([[result textContent] containsString:@"“The quick brown fox jumped over the lazy dog”"]); |
| #if ENABLE(TEXT_EXTRACTION_FILTER) |
| EXPECT_FALSE([[result textContent] containsString:@"Here’s to the crazy ones"]); |
| EXPECT_TRUE([result filteredOutAnyText]); |
| #endif |
| } |
| { |
| RetainPtr result = extractTextWithFilterOptions(_WKTextExtractionFilterClassifier); |
| EXPECT_TRUE([[result textContent] containsString:@"“The quick brown fox jumped over the lazy dog”"]); |
| EXPECT_TRUE([[result textContent] containsString:@"Here’s to the crazy ones"]); |
| EXPECT_FALSE([result filteredOutAnyText]); |
| } |
| } |
| |
| TEST(TextExtractionTests, FilterRedundantTextInLinks) |
| { |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| |
| [webView synchronouslyLoadHTMLString:@"<body>" |
| "<a class='first' href='http://apple.com'>apple</a>" |
| "<a class='second' href='http://webkit.org'>webkit</a>" |
| "</body>"]; |
| |
| RetainPtr world = [WKContentWorld _worldWithConfiguration:^{ |
| RetainPtr configuration = adoptNS([_WKContentWorldConfiguration new]); |
| [configuration setAllowJSHandleCreation:YES]; |
| return configuration.autorelease(); |
| }()]; |
| |
| RetainPtr debugText = [webView synchronouslyGetDebugText:^{ |
| RetainPtr firstLink = [webView querySelector:@".first" frame:nil world:world.get()]; |
| RetainPtr secondLink = [webView querySelector:@".second" frame:nil world:world.get()]; |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setIncludeURLs:NO]; |
| [configuration setIncludeRects:NO]; |
| [configuration setNodeIdentifierInclusion:_WKTextExtractionNodeIdentifierInclusionNone]; |
| [configuration addClientAttribute:@"href" value:@"url1.com" forNode:firstLink.get()]; |
| [configuration addClientAttribute:@"href" value:@"webkit.org" forNode:secondLink.get()]; |
| return configuration.autorelease(); |
| }()]; |
| |
| EXPECT_TRUE([debugText containsString:@"link,href='url1.com','apple'"]); |
| EXPECT_TRUE([debugText containsString:@"link,href='webkit.org'"]); |
| } |
| |
| TEST(TextExtractionTests, NodesToSkip) |
| { |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr world = [WKContentWorld _worldWithConfiguration:^{ |
| RetainPtr configuration = adoptNS([_WKContentWorldConfiguration new]); |
| [configuration setAllowJSHandleCreation:YES]; |
| return configuration.autorelease(); |
| }()]; |
| |
| RetainPtr selectHandle = [webView querySelector:@"select" frame:nil world:world.get()]; |
| RetainPtr inputHandle = [webView querySelector:@"input[type=email]" frame:nil world:world.get()]; |
| RetainPtr editorHandle = [webView querySelector:@"div[contenteditable]" frame:nil world:world.get()]; |
| RetainPtr hiddenTextHandle = [webView querySelector:@"h4" frame:nil world:world.get()]; |
| RetainPtr debugText = [webView synchronouslyGetDebugText:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setNodesToSkip:@[editorHandle.get(), selectHandle.get(), inputHandle.get(), hiddenTextHandle.get()]]; |
| [configuration setOutputFormat:_WKTextExtractionOutputFormatMarkdown]; |
| [configuration setNodeIdentifierInclusion:_WKTextExtractionNodeIdentifierInclusionNone]; |
| [configuration setIncludeRects:NO]; |
| return configuration.autorelease(); |
| }()]; |
| |
| NSArray<NSString *> *lines = [debugText componentsSeparatedByString:@"\n"]; |
| EXPECT_EQ([lines count], 2u); |
| EXPECT_WK_STREQ("Test", lines[0]); |
| EXPECT_WK_STREQ("0", lines[1]); |
| } |
| |
| TEST(TextExtractionTests, RequestJSHandleForNodeIdentifier) |
| { |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr extractionResult = [webView synchronouslyExtractDebugTextResult:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setIncludeRects:NO]; |
| [configuration setIncludeURLs:NO]; |
| return configuration.autorelease(); |
| }()]; |
| |
| RetainPtr debugTextForSubject = [webView synchronouslyGetDebugText:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setIncludeRects:NO]; |
| [configuration setIncludeURLs:NO]; |
| [configuration setNodeIdentifierInclusion:_WKTextExtractionNodeIdentifierInclusionNone]; |
| |
| RetainPtr nodeID = extractNodeIdentifier([extractionResult textContent], @"Compose a new message"); |
| [configuration setTargetNode:[extractionResult jsHandleForNodeIdentifier:nodeID.get() searchText:@"Subject"]]; |
| return configuration.autorelease(); |
| }()]; |
| |
| EXPECT_WK_STREQ(debugTextForSubject.get(), @"root\n\taria-label='Heading','Subject'"); |
| |
| RetainPtr debugTextForBody = [webView synchronouslyGetDebugText:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setIncludeRects:NO]; |
| [configuration setIncludeURLs:NO]; |
| [configuration setNodeIdentifierInclusion:_WKTextExtractionNodeIdentifierInclusionNone]; |
| [configuration setTargetNode:[extractionResult jsHandleForNodeIdentifier:nil searchText:@"The quick brown fox"]]; |
| return configuration.autorelease(); |
| }()]; |
| |
| EXPECT_WK_STREQ(debugTextForBody.get(), @"root,'“The quick brown fox jumped over the lazy dog”'"); |
| } |
| |
| TEST(TextExtractionTests, ResolveTargetNodeFromSelectorData) |
| { |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| |
| RetainPtr selectorData = [&] { |
| RetainPtr originalWebView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]); |
| [originalWebView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr world = [WKContentWorld _worldWithConfiguration:^{ |
| RetainPtr configuration = adoptNS([_WKContentWorldConfiguration new]); |
| [configuration setAllowJSHandleCreation:YES]; |
| return configuration.autorelease(); |
| }()]; |
| |
| RetainPtr subjectHandle = [originalWebView querySelector:@"h3" frame:nil world:world.get()]; |
| return [originalWebView synchronouslyGetSelectorPathDataForNode:subjectHandle.get()]; |
| }(); |
| |
| EXPECT_NOT_NULL(selectorData.get()); |
| |
| RetainPtr newWebView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration.get()]); |
| [newWebView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr resolvedHandle = [newWebView synchronouslyGetNodeForSelectorPathData:selectorData.get()]; |
| EXPECT_NOT_NULL(resolvedHandle.get()); |
| |
| RetainPtr debugText = [newWebView synchronouslyGetDebugText:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setIncludeRects:NO]; |
| [configuration setIncludeURLs:NO]; |
| [configuration setNodeIdentifierInclusion:_WKTextExtractionNodeIdentifierInclusionNone]; |
| [configuration setTargetNode:resolvedHandle.get()]; |
| return configuration.autorelease(); |
| }()]; |
| |
| EXPECT_WK_STREQ(debugText.get(), @"root\n\taria-label='Heading','Subject'"); |
| } |
| |
| #if HAVE(SAFARI_SAFE_BROWSING_NAMESPACED_LISTS) |
| |
| typedef void(^WKSafeBrowsingNamespacedListBlock)(NSDictionary<NSString *, NSArray<NSString *> *> *, NSError *); |
| static void overrideGetListsForNamespace(SSBLookupContext *instance, SEL, NSString *, NSString *, WKSafeBrowsingNamespacedListBlock completion) |
| { |
| static NeverDestroyed workQueue = WorkQueue::create("Queue for simulating SSB API"_s); |
| workQueue.get()->dispatch([completion = makeBlockPtr(completion)] { |
| RetainPtr hardCodedResult = @{ |
| @"test1/domains": @[ @".*" ], |
| @"test1/filter": @[ @"return input.length >= 1000 ? '<too long>' : null" ], |
| @"test2/domains": @[ @".*" ], |
| @"test2/filter": @[ @"return input.replaceAll('o', '•').replaceAll('u', 'v')" ], |
| }; |
| completion(hardCodedResult.get(), nil); |
| }); |
| } |
| |
| TEST(TextExtractionTests, FilteringRules) |
| { |
| InstanceMethodSwizzler safeBrowsingSwizzler { |
| getSSBLookupContextClassSingleton(), |
| @selector(_getListsForNamespace:collectionId:completionHandler:), |
| reinterpret_cast<IMP>(overrideGetListsForNamespace) |
| }; |
| |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| |
| [webView synchronouslyLoadTestPageNamed:@"debug-text-extraction"]; |
| |
| RetainPtr debugText = [webView synchronouslyGetDebugText:^{ |
| RetainPtr configuration = adoptNS([_WKTextExtractionConfiguration new]); |
| [configuration setOutputFormat:_WKTextExtractionOutputFormatHTML]; |
| [configuration setNodeIdentifierInclusion:_WKTextExtractionNodeIdentifierInclusionNone]; |
| [configuration setIncludeRects:NO]; |
| return configuration.autorelease(); |
| }()]; |
| |
| EXPECT_TRUE([debugText containsString:@"<input type='email' placeholder='Recipient address'>f••</input>"]); |
| EXPECT_TRUE([debugText containsString:@"<h3 aria-label='Heading'>Svbject</h3>"]); |
| EXPECT_TRUE([debugText containsString:@"The qvick br•wn f•x jvmped •ver the lazy d•g"]); |
| } |
| |
| #endif // HAVE(SAFARI_SAFE_BROWSING_NAMESPACED_LISTS) |
| |
| static String mainFrameMarkup(uint16_t port) |
| { |
| return makeString(R"(<!DOCTYPE html> |
| <html> |
| <head> |
| <meta name='viewport' content='width=device-width, initial-scale=1'> |
| <style> |
| iframe { |
| width: 300px; |
| height: 300px; |
| } |
| </style> |
| </head> |
| <body> |
| <iframe class='same'></iframe> |
| <iframe class='cross'></iframe> |
| <a href='https://webkit.org'>Link to WebKit home page</a> |
| <script> |
| subframeLoadedCount = 0; |
| sameOriginFrame = document.querySelector('iframe.same'); |
| sameOriginFrame.addEventListener('load', () => subframeLoadedCount++, { once: true }); |
| sameOriginFrame.src = 'subframe-same.html'; |
| |
| crossOriginFrame = document.querySelector('iframe.cross'); |
| crossOriginFrame.addEventListener('load', () => subframeLoadedCount++, { once: true }); |
| crossOriginFrame.src = 'http://localhost:)"_s, port, R"(/subframe-cross.html'; |
| </script> |
| </body> |
| </html>)"_s); |
| } |
| |
| static String subFrameMarkup(ASCIILiteral buttonText) |
| { |
| return makeString(R"(<!DOCTYPE html> |
| <html> |
| <body> |
| <h1>Click count: <span id='click-count'>0</span></h1> |
| <article aria-label='Button container'> |
| <button>)"_s, buttonText, R"(</button> |
| </article> |
| <script> |
| const clickCount = document.getElementById('click-count'); |
| const button = document.querySelector('button'); |
| button.addEventListener('click', () => { |
| clickCount.textContent = 1 + parseInt(clickCount.textContent); |
| }); |
| </script> |
| </body> |
| </html>)"_s); |
| } |
| |
| TEST(TextExtractionTests, SubframeInteractions) |
| { |
| HTTPServer server { { |
| { "/subframe-cross.html"_s, { subFrameMarkup("Cross origin: click here"_s) } }, |
| { "/subframe-same.html"_s, { subFrameMarkup("Same origin: click here"_s) } }, |
| }, HTTPServer::Protocol::Http }; |
| |
| server.addResponse("/"_s, { mainFrameMarkup(server.port()) }); |
| |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 400, 400) configuration:^{ |
| RetainPtr configuration = adoptNS([[WKWebViewConfiguration alloc] init]); |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| |
| __block RetainPtr subframes = adoptNS([NSMutableArray new]); |
| RetainPtr navigationDelegate = adoptNS([TestNavigationDelegate new]); |
| [navigationDelegate setDidCommitLoadWithRequestInFrame:^(WKWebView *, NSURLRequest *, WKFrameInfo *frame) { |
| if (!frame.mainFrame && ![frame.request.URL.scheme isEqualToString:@"about"]) |
| [subframes addObject:frame]; |
| }]; |
| [webView setNavigationDelegate:navigationDelegate.get()]; |
| [webView loadRequest:server.request()]; |
| [navigationDelegate waitForDidFinishNavigation]; |
| |
| Util::waitForConditionWithLogging([webView] { |
| return [[webView objectByEvaluatingJavaScript:@"subframeLoadedCount"] intValue] == 2; |
| }, 2, @"Expected subframes to finish loading."); |
| |
| RetainPtr extractionConfiguration = adoptNS([_WKTextExtractionConfiguration new]); |
| [extractionConfiguration setIncludeRects:NO]; |
| [extractionConfiguration setIncludeURLs:NO]; |
| [extractionConfiguration setAdditionalFrames:subframes.get()]; |
| |
| RetainPtr world = [WKContentWorld _worldWithConfiguration:^{ |
| RetainPtr configuration = adoptNS([_WKContentWorldConfiguration new]); |
| [configuration setAllowJSHandleCreation:YES]; |
| return configuration.autorelease(); |
| }()]; |
| |
| for (WKFrameInfo *subframe in subframes.get()) { |
| RetainPtr button = [webView querySelector:@"button" frame:subframe world:world.get()]; |
| EXPECT_NOT_NULL(button); |
| [extractionConfiguration addClientAttribute:@"foo" value:@"bar" forNode:button.get()]; |
| } |
| |
| auto numberOfMatches = [](NSString *text, NSString *patternString) { |
| RetainPtr pattern = [NSRegularExpression regularExpressionWithPattern:patternString options:0 error:nil]; |
| return [pattern numberOfMatchesInString:text options:0 range:NSMakeRange(0, text.length)]; |
| }; |
| |
| RetainPtr debugText = [webView synchronouslyGetDebugText:extractionConfiguration.get()]; |
| EXPECT_EQ(numberOfMatches(debugText.get(), @"foo='bar'"), 2u); |
| |
| { |
| RetainPtr interaction = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionClick]); |
| [interaction setNodeIdentifier:extractNodeIdentifier(debugText.get(), @"Same origin: click here")]; |
| |
| NSError *error = nil; |
| RetainPtr description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| EXPECT_WK_STREQ(description.get(), @"Click on button under article labeled “Button container”, with rendered text “Same origin: click here”"); |
| |
| RetainPtr result = [webView synchronouslyPerformInteraction:interaction.get()]; |
| EXPECT_NULL([result error]); |
| } |
| { |
| RetainPtr interaction = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionClick]); |
| [interaction setNodeIdentifier:extractNodeIdentifier(debugText.get(), @"Cross origin: click here")]; |
| |
| NSError *error = nil; |
| RetainPtr description = [interaction debugDescriptionInWebView:webView.get() error:&error]; |
| EXPECT_WK_STREQ(description.get(), @"Click on button under article labeled “Button container”, with rendered text “Cross origin: click here”"); |
| |
| RetainPtr result = [webView synchronouslyPerformInteraction:interaction.get()]; |
| EXPECT_NULL([result error]); |
| } |
| |
| RetainPtr debugTextAfterClicks = [webView synchronouslyGetDebugText:extractionConfiguration.get()]; |
| EXPECT_EQ(numberOfMatches(debugTextAfterClicks.get(), @"Click count: 1"), 2u); |
| } |
| |
| TEST(TextExtractionTests, InjectedBundle) |
| { |
| WKWebViewConfiguration *configuration = [WKWebViewConfiguration _test_configurationWithTestPlugInClassName:@"JSHandlePlugIn"]; |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 800, 600) configuration:configuration]); |
| RetainPtr receiver = adoptNS([JSHandleReceiver new]); |
| __block RetainPtr<_WKJSHandle> handle; |
| __block RetainPtr<_WKJSHandle> decodedHandle; |
| receiver.get().dictionaryReceiver = ^(NSDictionary *dictionary) { |
| handle = dynamic_objc_cast<_WKJSHandle>(dictionary[@"testkey"]); |
| decodedHandle = [NSKeyedUnarchiver _strictlyUnarchivedObjectOfClasses:[NSSet setWithObject:_WKJSHandle.class] fromData:dynamic_objc_cast<NSData>(dictionary[@"testdatakey"]) error:nil]; |
| }; |
| |
| _WKRemoteObjectInterface *interface = [_WKRemoteObjectInterface remoteObjectInterfaceWithProtocol:@protocol(JSHandlePlugInProtocol)]; |
| [interface setClasses:[NSSet setWithObjects:NSDictionary.class, NSString.class, _WKJSHandle.class, nil] forSelector:@selector(receiveDictionaryFromWebProcess:) argumentIndex:0 ofReply:NO]; |
| [[webView _remoteObjectRegistry] registerExportedObject:receiver.get() interface:interface]; |
| |
| [webView loadHTMLString:@"text outside <div id='testelement'> text inside </div>" baseURL:nil]; |
| while (!handle) |
| Util::spinRunLoop(); |
| EXPECT_NOT_NULL(handle.get().frame._documentIdentifier); |
| EXPECT_TRUE([handle.get().frame._documentIdentifier isEqual:decodedHandle.get().frame._documentIdentifier]); |
| EXPECT_TRUE([handle.get().frame._documentIdentifier isEqual:[webView mainFrame].info._documentIdentifier]); |
| EXPECT_TRUE([handle.get().frame _isSameFrame:[webView mainFrame].info]); |
| } |
| |
| TEST(TextExtractionTests, ClickInteractionWhileInBackground) |
| { |
| RetainPtr webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 400, 400) configuration:^{ |
| RetainPtr configuration = adoptNS([WKWebViewConfiguration new]); |
| [configuration _setBackgroundTextExtractionEnabled:YES]; |
| [[configuration preferences] _setTextExtractionEnabled:YES]; |
| return configuration.autorelease(); |
| }()]); |
| |
| [webView synchronouslyLoadHTMLString:@R"HTML( |
| <!DOCTYPE html> |
| <html> |
| <head> |
| <meta name='viewport' content='width=device-width, initial-scale=1'> |
| </head> |
| <body> |
| <button>Click Me</button> |
| <div id='result'>pending</div> |
| <script> |
| document.querySelector('button').addEventListener('click', async () => { |
| for (let i = 0; i < 3; ++i) { |
| await new Promise(resolve => setTimeout(resolve, 50)); |
| await new Promise(requestAnimationFrame); |
| } |
| document.getElementById('result').textContent = 'completed'; |
| }); |
| </script> |
| </body> |
| </html> |
| )HTML"]; |
| |
| #if PLATFORM(IOS_FAMILY) |
| [NSNotificationCenter.defaultCenter postNotificationName:UIApplicationDidEnterBackgroundNotification object:UIApplication.sharedApplication userInfo:@{@"isSuspendedUnderLock": @NO }]; |
| [NSNotificationCenter.defaultCenter postNotificationName:UISceneDidEnterBackgroundNotification object:[[webView window] windowScene] userInfo:nil]; |
| #else |
| [[webView window] orderOut:nil]; |
| #endif |
| |
| RetainPtr debugText = [webView synchronouslyGetDebugText:nil]; |
| RetainPtr buttonID = extractNodeIdentifier(debugText.get(), @"Click Me"); |
| EXPECT_NOT_NULL(buttonID.get()); |
| |
| RetainPtr click = adoptNS([[_WKTextExtractionInteraction alloc] initWithAction:_WKTextExtractionActionClick]); |
| [click setNodeIdentifier:buttonID.get()]; |
| |
| RetainPtr result = [webView synchronouslyPerformInteraction:click.get()]; |
| EXPECT_NULL([result error]); |
| |
| Util::waitForConditionWithLogging([webView] { |
| return [[webView stringByEvaluatingJavaScript:@"document.getElementById('result').textContent"] isEqualToString:@"completed"]; |
| }, 5, @"Expected result text to become 'completed'."); |
| } |
| |
| } // namespace TestWebKitAPI |