/* * Copyright (C) 2015 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 "PlatformUtilities.h" #import "Test.h" #import "TestNavigationDelegate.h" #import "TestWKWebView.h" #import "WKWebViewConfigurationExtras.h" #import #import #import #if PLATFORM(IOS_FAMILY) #import "TestInputDelegate.h" #import "UIKitSPI.h" #endif #if !PLATFORM(IOS_FAMILY) typedef enum : NSUInteger { NSTextFinderAsynchronousDocumentFindOptionsBackwards = 1 << 0, NSTextFinderAsynchronousDocumentFindOptionsWrap = 1 << 1, } NSTextFinderAsynchronousDocumentFindOptions; constexpr auto noFindOptions = (NSTextFinderAsynchronousDocumentFindOptions)0; constexpr auto backwardsFindOptions = NSTextFinderAsynchronousDocumentFindOptionsBackwards; constexpr auto wrapFindOptions = NSTextFinderAsynchronousDocumentFindOptionsWrap; constexpr auto wrapBackwardsFindOptions = (NSTextFinderAsynchronousDocumentFindOptions)(NSTextFinderAsynchronousDocumentFindOptionsWrap | NSTextFinderAsynchronousDocumentFindOptionsBackwards); @protocol NSTextFinderAsynchronousDocumentFindMatch @property (retain, nonatomic, readonly) NSArray *textRects; - (void)generateTextImage:(void (^)(NSImage *generatedImage))completionHandler; @end typedef id FindMatch; @interface WKWebView (NSTextFinderSupport) - (void)findMatchesForString:(NSString *)targetString relativeToMatch:(FindMatch)relativeMatch findOptions:(NSTextFinderAsynchronousDocumentFindOptions)findOptions maxResults:(NSUInteger)maxResults resultCollector:(void (^)(NSArray *matches, BOOL didWrap))resultCollector; - (void)replaceMatches:(NSArray *)matches withString:(NSString *)replacementString inSelectionOnly:(BOOL)selectionOnly resultCollector:(void (^)(NSUInteger replacementCount))resultCollector; @end struct FindResult { RetainPtr matches; BOOL didWrap { NO }; }; static FindResult findMatches(WKWebView *webView, NSString *findString, NSTextFinderAsynchronousDocumentFindOptions findOptions = noFindOptions, NSUInteger maxResults = NSUIntegerMax) { __block FindResult result; __block bool done = false; [webView findMatchesForString:findString relativeToMatch:nil findOptions:findOptions maxResults:maxResults resultCollector:^(NSArray *matches, BOOL didWrap) { result.matches = matches; result.didWrap = didWrap; done = true; }]; TestWebKitAPI::Util::run(&done); return result; } static NSUInteger replaceMatches(WKWebView *webView, NSArray *matchesToReplace, NSString *replacementText) { __block NSUInteger result; __block bool done = false; [webView replaceMatches:matchesToReplace withString:replacementText inSelectionOnly:NO resultCollector:^(NSUInteger replacementCount) { result = replacementCount; done = true; }]; TestWebKitAPI::Util::run(&done); return result; } TEST(WebKit, FindInPage) { RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)]); [webView _setOverrideDeviceScaleFactor:2]; NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"lots-of-text" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]; [webView loadRequest:request]; [webView _test_waitForDidFinishNavigation]; // Find all matches. auto result = findMatches(webView.get(), @"Birthday"); EXPECT_EQ((NSUInteger)360, [result.matches count]); RetainPtr match = [result.matches objectAtIndex:0]; EXPECT_EQ((NSUInteger)1, [match textRects].count); // Ensure that the generated image has the correct DPI. __block bool generateTextImageDone = false; [match generateTextImage:^(NSImage *image) { CGImageRef CGImage = [image CGImageForProposedRect:nil context:nil hints:nil]; EXPECT_EQ(image.size.width, CGImageGetWidth(CGImage) / 2); EXPECT_EQ(image.size.height, CGImageGetHeight(CGImage) / 2); generateTextImageDone = true; }]; TestWebKitAPI::Util::run(&generateTextImageDone); // Find one match, doing an incremental search. result = findMatches(webView.get(), @"Birthday", noFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); RetainPtr firstMatch = [result.matches firstObject]; EXPECT_EQ((NSUInteger)1, [firstMatch textRects].count); // Find the next match in incremental mode. result = findMatches(webView.get(), @"Birthday", noFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); RetainPtr secondMatch = [result.matches firstObject]; EXPECT_EQ((NSUInteger)1, [secondMatch textRects].count); EXPECT_FALSE(NSEqualRects([[firstMatch textRects].lastObject rectValue], [[secondMatch textRects].lastObject rectValue])); // Find the previous match in incremental mode. result = findMatches(webView.get(), @"Birthday", backwardsFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); RetainPtr firstMatchAgain = [result.matches firstObject]; EXPECT_EQ((NSUInteger)1, [firstMatchAgain textRects].count); EXPECT_TRUE(NSEqualRects([[firstMatch textRects].lastObject rectValue], [[firstMatchAgain textRects].lastObject rectValue])); // Ensure that we cap the number of matches. There are actually 1600, but we only get the first 1000. result = findMatches(webView.get(), @" "); EXPECT_EQ((NSUInteger)1000, [result.matches count]); } TEST(WebKit, FindInPageWithPlatformPresentation) { // This should be the same as above, but does not generate rects or images, so that AppKit won't paint its find UI. RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 200, 200)]); [webView _setOverrideDeviceScaleFactor:2]; [webView _setUsePlatformFindUI:NO]; NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"lots-of-text" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]; [webView loadRequest:request]; [webView _test_waitForDidFinishNavigation]; // Find all matches, but recieve no rects. auto result = findMatches(webView.get(), @"Birthday"); EXPECT_EQ((NSUInteger)360, [result.matches count]); RetainPtr match = [result.matches objectAtIndex:0]; EXPECT_EQ((NSUInteger)0, [match textRects].count); // Ensure that no image is generated. __block bool generateTextImageDone = false; [match generateTextImage:^(NSImage *image) { EXPECT_EQ(image, nullptr); generateTextImageDone = true; }]; TestWebKitAPI::Util::run(&generateTextImageDone); // Ensure that we cap the number of matches. There are actually 1600, but we only get the first 1000. result = findMatches(webView.get(), @" "); EXPECT_EQ((NSUInteger)1000, [result.matches count]); } TEST(WebKit, FindInPageWrapping) { RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]); [webView _setOverrideDeviceScaleFactor:2]; [webView loadHTMLString:@"word word" baseURL:nil]; [webView _test_waitForDidFinishNavigation]; // Find one match, doing an incremental search. auto result = findMatches(webView.get(), @"word", wrapFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); EXPECT_FALSE(result.didWrap); result = findMatches(webView.get(), @"word", wrapFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); EXPECT_FALSE(result.didWrap); // The next match should wrap. result = findMatches(webView.get(), @"word", wrapFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); EXPECT_TRUE(result.didWrap); // Going backward after wrapping should wrap again. result = findMatches(webView.get(), @"word", wrapBackwardsFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); EXPECT_TRUE(result.didWrap); } TEST(WebKit, FindInPageWrappingDisabled) { RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]); [webView _setOverrideDeviceScaleFactor:2]; [webView loadHTMLString:@"word word" baseURL:nil]; [webView _test_waitForDidFinishNavigation]; // Find one match, doing an incremental search. auto result = findMatches(webView.get(), @"word", noFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); EXPECT_FALSE(result.didWrap); result = findMatches(webView.get(), @"word", noFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); EXPECT_FALSE(result.didWrap); // The next match should fail, because wrapping is disabled. result = findMatches(webView.get(), @"word", noFindOptions, 1); EXPECT_EQ((NSUInteger)0, [result.matches count]); EXPECT_FALSE(result.didWrap); } TEST(WebKit, FindInPageBackwardsFirst) { RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]); [webView _setOverrideDeviceScaleFactor:2]; [webView loadHTMLString:@"word word" baseURL:nil]; [webView _test_waitForDidFinishNavigation]; // Find one match, doing an incremental search. auto result = findMatches(webView.get(), @"word", wrapBackwardsFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); result = findMatches(webView.get(), @"word", wrapBackwardsFindOptions, 1); EXPECT_EQ((NSUInteger)1, [result.matches count]); } TEST(WebKit, FindInPageWrappingSubframe) { RetainPtr webView = adoptNS([[WKWebView alloc] initWithFrame:NSMakeRect(0, 0, 100, 100)]); [webView _setOverrideDeviceScaleFactor:2]; [webView loadHTMLString:@"word "]; auto ranges = textRangesForQueryString(webView.get(), @"Sapien"); __block bool done = false; [webView _requestRectForFoundTextRange:[ranges firstObject] completionHandler:^(CGRect rect) { EXPECT_TRUE(CGRectEqualToRect(rect, CGRectMake(252, 146, 44, 19))); done = true; }]; TestWebKitAPI::Util::run(&done); ranges = textRangesForQueryString(webView.get(), @"fermentum"); done = false; [webView _requestRectForFoundTextRange:[ranges firstObject] completionHandler:^(CGRect rect) { EXPECT_TRUE(CGRectEqualToRect(rect, CGRectMake(229, 646, 72, 19))); done = true; }]; TestWebKitAPI::Util::run(&done); [webView scrollRangeToVisible:[ranges firstObject] inDocument:nil]; done = false; [webView _requestRectForFoundTextRange:[ranges firstObject] completionHandler:^(CGRect rect) { EXPECT_TRUE(CGRectEqualToRect(rect, CGRectMake(229, 104, 72, 19))); done = true; }]; TestWebKitAPI::Util::run(&done); } TEST(WebKit, ScrollToFoundRangeWithExistingSelection) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)]); [webView synchronouslyLoadHTMLString:@"

Top

Bottom

"]; [webView objectByEvaluatingJavaScript:@"let p = document.querySelector('p'); document.getSelection().setBaseAndExtent(p, 0, p, 1)"]; auto scrollViewDelegate = adoptNS([[TestScrollViewDelegate alloc] init]); [webView scrollView].delegate = scrollViewDelegate.get(); auto ranges = textRangesForQueryString(webView.get(), @"Bottom"); [webView scrollRangeToVisible:[ranges firstObject] inDocument:nil]; TestWebKitAPI::Util::run(&scrollViewDelegate->_finishedScrolling); EXPECT_TRUE(CGPointEqualToPoint([webView scrollView].contentOffset, CGPointMake(0, 664))); } TEST(WebKit, ScrollToFoundRangeDoesNotFocusElement) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)]); [webView synchronouslyLoadHTMLString:@"

Top

Bottom

"]; auto scrollViewDelegate = adoptNS([[TestScrollViewDelegate alloc] init]); [webView scrollView].delegate = scrollViewDelegate.get(); bool inputFocused = false; auto inputDelegate = adoptNS([TestInputDelegate new]); [inputDelegate setFocusStartsInputSessionPolicyHandler:[&inputFocused] (WKWebView *, id<_WKFocusedElementInfo> focusedElementInfo) -> _WKFocusStartsInputSessionPolicy { switch (focusedElementInfo.type) { case WKInputTypeText: inputFocused = true; break; default: ADD_FAILURE() << "Unexpected focus change."; break; } return _WKFocusStartsInputSessionPolicyAllow; }]; [webView _setInputDelegate:inputDelegate.get()]; [webView evaluateJavaScript:@"document.getElementById('input').focus()" completionHandler:nil]; TestWebKitAPI::Util::run(&inputFocused); auto ranges = textRangesForQueryString(webView.get(), @"Bottom"); [webView scrollRangeToVisible:[ranges firstObject] inDocument:nil]; TestWebKitAPI::Util::run(&scrollViewDelegate->_finishedScrolling); } TEST(WebKit, CannotHaveMultipleFindOverlays) { auto webView = adoptNS([[TestWKWebView alloc] initWithFrame:CGRectMake(0, 0, 200, 200)]); NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:@"lots-of-text" withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]]; [webView loadRequest:request]; [webView _test_waitForDidFinishNavigation]; EXPECT_EQ(overlayCount(webView.get()), 0U); [webView didBeginTextSearchOperation]; // Wait for two presentation updates, as the document overlay root layer is // created lazily. [webView waitForNextPresentationUpdate]; [webView waitForNextPresentationUpdate]; EXPECT_EQ(overlayCount(webView.get()), 1U); [webView didEndTextSearchOperation]; [webView didBeginTextSearchOperation]; [webView waitForNextPresentationUpdate]; [webView waitForNextPresentationUpdate]; EXPECT_EQ(overlayCount(webView.get()), 1U); } #endif // HAVE(UIFINDINTERACTION)