EXC_BAD_ACCESS when invoking a DownloadProxy's destination decision handler after...
[WebKit-https.git] / Tools / TestWebKitAPI / cocoa / TestWKWebView.mm
1 /*
2  * Copyright (C) 2016 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "TestWKWebView.h"
28
29 #if WK_API_ENABLED
30
31 #import "ClassMethodSwizzler.h"
32 #import "TestNavigationDelegate.h"
33 #import "Utilities.h"
34
35 #import <WebKit/WKWebViewConfigurationPrivate.h>
36 #import <WebKit/WebKitPrivate.h>
37 #import <WebKit/_WKActivatedElementInfo.h>
38 #import <WebKit/_WKProcessPoolConfiguration.h>
39 #import <objc/runtime.h>
40 #import <wtf/RetainPtr.h>
41
42 #if PLATFORM(MAC)
43 #import <AppKit/AppKit.h>
44 #import <Carbon/Carbon.h>
45 #import <wtf/mac/AppKitCompatibilityDeclarations.h>
46 #endif
47
48 #if PLATFORM(IOS_FAMILY)
49 #import "UIKitSPI.h"
50 #import <wtf/SoftLinking.h>
51 SOFT_LINK_FRAMEWORK(UIKit)
52 SOFT_LINK_CLASS(UIKit, UIWindow)
53
54 @implementation WKWebView (WKWebViewTestingQuirks)
55
56 // TestWebKitAPI is currently not a UIApplication so we are unable to track if it is in
57 // the background or not (https://bugs.webkit.org/show_bug.cgi?id=175204). This can
58 // cause our processes to get suspended on iOS. We work around this by having
59 // WKWebView._isBackground always return NO in the context of API tests.
60 - (BOOL)_isBackground
61 {
62     return NO;
63 }
64 @end
65 #endif
66
67 @implementation WKWebView (TestWebKitAPI)
68
69 - (BOOL)_synchronouslyExecuteEditCommand:(NSString *)command argument:(NSString *)argument
70 {
71     __block bool done = false;
72     __block bool success;
73     [self _executeEditCommand:command argument:argument completion:^(BOOL completionSuccess) {
74         done = true;
75         success = completionSuccess;
76     }];
77     TestWebKitAPI::Util::run(&done);
78     return success;
79 }
80
81 @end
82
83 @implementation TestMessageHandler {
84     NSMutableDictionary<NSString *, dispatch_block_t> *_messageHandlers;
85 }
86
87 - (void)addMessage:(NSString *)message withHandler:(dispatch_block_t)handler
88 {
89     if (!_messageHandlers)
90         _messageHandlers = [NSMutableDictionary dictionary];
91
92     _messageHandlers[message] = [handler copy];
93 }
94
95 - (void)removeMessage:(NSString *)message
96 {
97     [_messageHandlers removeObjectForKey:message];
98 }
99
100 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
101 {
102     dispatch_block_t handler = _messageHandlers[message.body];
103     if (handler)
104         handler();
105 }
106
107 @end
108
109 #if PLATFORM(MAC)
110 @interface TestWKWebViewHostWindow : NSWindow
111 #else
112 @interface TestWKWebViewHostWindow : UIWindow
113 #endif // PLATFORM(MAC)
114 @end
115
116 @implementation TestWKWebViewHostWindow {
117     BOOL _forceKeyWindow;
118 }
119
120 #if PLATFORM(MAC)
121 static int gEventNumber = 1;
122
123 NSEventMask __simulated_forceClickAssociatedEventsMask(id self, SEL _cmd)
124 {
125     return NSEventMaskPressure | NSEventMaskLeftMouseDown | NSEventMaskLeftMouseUp | NSEventMaskLeftMouseDragged;
126 }
127
128 - (void)_mouseDownAtPoint:(NSPoint)point simulatePressure:(BOOL)simulatePressure clickCount:(NSUInteger)clickCount
129 {
130     NSEventType mouseEventType = NSEventTypeLeftMouseDown;
131
132     NSEventMask modifierFlags = 0;
133     if (simulatePressure)
134         modifierFlags |= NSEventMaskPressure;
135
136     NSEvent *event = [NSEvent mouseEventWithType:mouseEventType location:point modifierFlags:modifierFlags timestamp:GetCurrentEventTime() windowNumber:self.windowNumber context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:clickCount pressure:simulatePressure];
137     if (!simulatePressure) {
138         [self sendEvent:event];
139         return;
140     }
141
142     IMP simulatedAssociatedEventsMaskImpl = (IMP)__simulated_forceClickAssociatedEventsMask;
143     Method associatedEventsMaskMethod = class_getInstanceMethod([NSEvent class], @selector(associatedEventsMask));
144     IMP originalAssociatedEventsMaskImpl = method_setImplementation(associatedEventsMaskMethod, simulatedAssociatedEventsMaskImpl);
145     @try {
146         [self sendEvent:event];
147     } @finally {
148         // In the case where event sending raises an exception, we still want to restore the original implementation
149         // to prevent subsequent event sending tests from being affected.
150         method_setImplementation(associatedEventsMaskMethod, originalAssociatedEventsMaskImpl);
151     }
152 }
153
154 - (void)_mouseUpAtPoint:(NSPoint)point clickCount:(NSUInteger)clickCount
155 {
156     [self sendEvent:[NSEvent mouseEventWithType:NSEventTypeLeftMouseUp location:point modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:self.windowNumber context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:clickCount pressure:0]];
157 }
158 #endif // PLATFORM(MAC)
159
160 - (BOOL)isKeyWindow
161 {
162     return _forceKeyWindow || [super isKeyWindow];
163 }
164
165 - (void)makeKeyWindow
166 {
167     if (_forceKeyWindow)
168         return;
169
170     _forceKeyWindow = YES;
171 #if PLATFORM(MAC)
172     [[NSNotificationCenter defaultCenter] postNotificationName:NSWindowDidBecomeKeyNotification object:self];
173 #else
174     [[NSNotificationCenter defaultCenter] postNotificationName:UIWindowDidBecomeKeyNotification object:self];
175 #endif
176 }
177
178 - (void)resignKeyWindow
179 {
180     _forceKeyWindow = NO;
181     [super resignKeyWindow];
182 }
183
184 @end
185
186 @implementation TestWKWebView {
187     RetainPtr<TestWKWebViewHostWindow> _hostWindow;
188     RetainPtr<TestMessageHandler> _testHandler;
189 #if PLATFORM(IOS_FAMILY)
190     std::unique_ptr<TestWebKitAPI::ClassMethodSwizzler> _sharedCalloutBarSwizzler;
191 #endif
192 }
193
194 - (instancetype)initWithFrame:(CGRect)frame
195 {
196     WKWebViewConfiguration *defaultConfiguration = [[[WKWebViewConfiguration alloc] init] autorelease];
197     return [self initWithFrame:frame configuration:defaultConfiguration];
198 }
199
200 - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
201 {
202     return [self initWithFrame:frame configuration:configuration addToWindow:YES];
203 }
204
205 #if PLATFORM(IOS_FAMILY)
206
207 static UICalloutBar *suppressUICalloutBar()
208 {
209     return nil;
210 }
211
212 #endif
213
214 - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration addToWindow:(BOOL)addToWindow
215 {
216     self = [super initWithFrame:frame configuration:configuration];
217     if (!self)
218         return nil;
219
220     if (addToWindow)
221         [self _setUpTestWindow:frame];
222
223 #if PLATFORM(IOS_FAMILY)
224     // FIXME: Remove this workaround once <https://webkit.org/b/175204> is fixed.
225     _sharedCalloutBarSwizzler = std::make_unique<TestWebKitAPI::ClassMethodSwizzler>([UICalloutBar class], @selector(sharedCalloutBar), reinterpret_cast<IMP>(suppressUICalloutBar));
226 #endif
227
228     return self;
229 }
230
231 - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration processPoolConfiguration:(_WKProcessPoolConfiguration *)processPoolConfiguration
232 {
233     [configuration setProcessPool:[[[WKProcessPool alloc] _initWithConfiguration:processPoolConfiguration] autorelease]];
234     return [self initWithFrame:frame configuration:configuration];
235 }
236
237 - (void)_setUpTestWindow:(NSRect)frame
238 {
239 #if PLATFORM(MAC)
240     _hostWindow = adoptNS([[TestWKWebViewHostWindow alloc] initWithContentRect:frame styleMask:NSWindowStyleMaskBorderless backing:NSBackingStoreBuffered defer:NO]);
241     [_hostWindow setFrameOrigin:NSMakePoint(0, 0)];
242     [_hostWindow setIsVisible:YES];
243     [_hostWindow contentView].wantsLayer = YES;
244     [[_hostWindow contentView] addSubview:self];
245     [_hostWindow makeKeyAndOrderFront:self];
246 #else
247     _hostWindow = adoptNS([[TestWKWebViewHostWindow alloc] initWithFrame:frame]);
248     [_hostWindow setHidden:NO];
249     [_hostWindow addSubview:self];
250 #endif
251 }
252
253 - (void)clearMessageHandlers:(NSArray *)messageNames
254 {
255     for (NSString *messageName in messageNames)
256         [_testHandler removeMessage:messageName];
257 }
258
259 - (void)performAfterReceivingMessage:(NSString *)message action:(dispatch_block_t)action
260 {
261     if (!_testHandler) {
262         _testHandler = adoptNS([[TestMessageHandler alloc] init]);
263         [[[self configuration] userContentController] addScriptMessageHandler:_testHandler.get() name:@"testHandler"];
264     }
265
266     [_testHandler addMessage:message withHandler:action];
267 }
268
269 - (void)loadTestPageNamed:(NSString *)pageName
270 {
271     NSURLRequest *request = [NSURLRequest requestWithURL:[[NSBundle mainBundle] URLForResource:pageName withExtension:@"html" subdirectory:@"TestWebKitAPI.resources"]];
272     [self loadRequest:request];
273 }
274
275 - (void)synchronouslyLoadHTMLString:(NSString *)html baseURL:(NSURL *)url
276 {
277     [self loadHTMLString:html baseURL:url];
278     [self _test_waitForDidFinishNavigation];
279 }
280
281 - (void)synchronouslyLoadHTMLString:(NSString *)html
282 {
283     [self synchronouslyLoadHTMLString:html baseURL:[[[NSBundle mainBundle] bundleURL] URLByAppendingPathComponent:@"TestWebKitAPI.resources"]];
284 }
285
286 - (void)synchronouslyLoadTestPageNamed:(NSString *)pageName
287 {
288     [self loadTestPageNamed:pageName];
289     [self _test_waitForDidFinishNavigation];
290 }
291
292 - (id)objectByEvaluatingJavaScript:(NSString *)script
293 {
294     bool isWaitingForJavaScript = false;
295     RetainPtr<id> evalResult;
296     [self _evaluateJavaScriptWithoutUserGesture:script completionHandler:[&] (id result, NSError *error) {
297         evalResult = result;
298         isWaitingForJavaScript = true;
299         EXPECT_TRUE(!error);
300         if (error)
301             NSLog(@"Encountered error: %@ while evaluating script: %@", error, script);
302     }];
303     TestWebKitAPI::Util::run(&isWaitingForJavaScript);
304     return evalResult.autorelease();
305 }
306
307 - (id)objectByEvaluatingJavaScriptWithUserGesture:(NSString *)script
308 {
309     bool isWaitingForJavaScript = false;
310     RetainPtr<id> evalResult;
311     [self evaluateJavaScript:script completionHandler:[&] (id result, NSError *error) {
312         evalResult = result;
313         isWaitingForJavaScript = true;
314         EXPECT_TRUE(!error);
315         if (error)
316             NSLog(@"Encountered error: %@ while evaluating script: %@", error, script);
317     }];
318     TestWebKitAPI::Util::run(&isWaitingForJavaScript);
319     return evalResult.autorelease();
320 }
321
322 - (NSString *)stringByEvaluatingJavaScript:(NSString *)script
323 {
324     return [NSString stringWithFormat:@"%@", [self objectByEvaluatingJavaScript:script]];
325 }
326
327 - (void)waitForMessage:(NSString *)message
328 {
329     __block bool isDoneWaiting = false;
330     [self performAfterReceivingMessage:message action:^()
331     {
332         isDoneWaiting = true;
333     }];
334     TestWebKitAPI::Util::run(&isDoneWaiting);
335 }
336
337 - (void)performAfterLoading:(dispatch_block_t)actions {
338     TestMessageHandler *handler = [[TestMessageHandler alloc] init];
339     [handler addMessage:@"loaded" withHandler:actions];
340
341     NSString *onloadScript = @"window.onload = () => window.webkit.messageHandlers.onloadHandler.postMessage('loaded')";
342     WKUserScript *script = [[WKUserScript alloc] initWithSource:onloadScript injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
343
344     WKUserContentController* contentController = [[self configuration] userContentController];
345     [contentController addUserScript:script];
346     [contentController addScriptMessageHandler:handler name:@"onloadHandler"];
347 }
348
349 - (void)waitForNextPresentationUpdate
350 {
351     __block bool done = false;
352     [self _doAfterNextPresentationUpdate:^() {
353         done = true;
354     }];
355
356     TestWebKitAPI::Util::run(&done);
357 }
358
359 - (NSString *)stylePropertyAtSelectionStart:(NSString *)propertyName
360 {
361     NSString *script = [NSString stringWithFormat:@"getComputedStyle(getSelection().getRangeAt(0).startContainer.parentElement)['%@']", propertyName];
362     return [self stringByEvaluatingJavaScript:script];
363 }
364
365 - (NSString *)stylePropertyAtSelectionEnd:(NSString *)propertyName
366 {
367     NSString *script = [NSString stringWithFormat:@"getComputedStyle(getSelection().getRangeAt(0).endContainer.parentElement)['%@']", propertyName];
368     return [self stringByEvaluatingJavaScript:script];
369 }
370
371 - (void)collapseToStart
372 {
373     [self evaluateJavaScript:@"getSelection().collapseToStart()" completionHandler:nil];
374 }
375
376 - (void)collapseToEnd
377 {
378     [self evaluateJavaScript:@"getSelection().collapseToEnd()" completionHandler:nil];
379 }
380
381 @end
382
383 #if PLATFORM(IOS_FAMILY)
384
385 @implementation TestWKWebView (IOSOnly)
386
387 - (UIView <UITextInputPrivate, UITextInputMultiDocument> *)textInputContentView
388 {
389     return (UIView <UITextInputPrivate, UITextInputMultiDocument> *)[self valueForKey:@"_currentContentView"];
390 }
391
392 - (RetainPtr<NSArray>)selectionRectsAfterPresentationUpdate
393 {
394     RetainPtr<TestWKWebView> retainedSelf = self;
395
396     __block bool isDone = false;
397     __block RetainPtr<NSArray> selectionRects;
398     [self _doAfterNextPresentationUpdate:^() {
399         selectionRects = adoptNS([[retainedSelf _uiTextSelectionRects] retain]);
400         isDone = true;
401     }];
402
403     TestWebKitAPI::Util::run(&isDone);
404     return selectionRects;
405 }
406
407 - (CGRect)caretViewRectInContentCoordinates
408 {
409     UIView *selectionView = [self.textInputContentView valueForKeyPath:@"interactionAssistant.selectionView"];
410     CGRect caretFrame = [[selectionView valueForKeyPath:@"caretView.frame"] CGRectValue];
411     return [selectionView convertRect:caretFrame toView:self.textInputContentView];
412 }
413
414 - (NSArray<NSValue *> *)selectionViewRectsInContentCoordinates
415 {
416     NSMutableArray *selectionRects = [NSMutableArray array];
417     NSArray<UITextSelectionRect *> *rects = [self.textInputContentView valueForKeyPath:@"interactionAssistant.selectionView.rangeView.rects"];
418     for (UITextSelectionRect *rect in rects)
419         [selectionRects addObject:[NSValue valueWithCGRect:rect.rect]];
420     return selectionRects;
421 }
422
423 - (_WKActivatedElementInfo *)activatedElementAtPosition:(CGPoint)position
424 {
425     __block RetainPtr<_WKActivatedElementInfo> info;
426     __block bool finished = false;
427     [self _requestActivatedElementAtPosition:position completionBlock:^(_WKActivatedElementInfo *elementInfo) {
428         info = elementInfo;
429         finished = true;
430     }];
431
432     TestWebKitAPI::Util::run(&finished);
433     return info.autorelease();
434 }
435
436 @end
437
438 #endif
439
440 #if PLATFORM(MAC)
441 @implementation TestWKWebView (MacOnly)
442 - (void)mouseDownAtPoint:(NSPoint)pointInWindow simulatePressure:(BOOL)simulatePressure
443 {
444     [_hostWindow _mouseDownAtPoint:pointInWindow simulatePressure:simulatePressure clickCount:1];
445 }
446
447 - (void)mouseUpAtPoint:(NSPoint)pointInWindow
448 {
449     [_hostWindow _mouseUpAtPoint:pointInWindow clickCount:1];
450 }
451
452 - (void)mouseMoveToPoint:(NSPoint)pointInWindow withFlags:(NSEventModifierFlags)flags
453 {
454     [self mouseMoved:[self _mouseEventWithType:NSEventTypeMouseMoved atLocation:pointInWindow flags:flags timestamp:GetCurrentEventTime() clickCount:0]];
455 }
456
457 - (void)sendClicksAtPoint:(NSPoint)pointInWindow numberOfClicks:(NSUInteger)numberOfClicks
458 {
459     for (NSUInteger clickCount = 1; clickCount <= numberOfClicks; ++clickCount) {
460         [_hostWindow _mouseDownAtPoint:pointInWindow simulatePressure:NO clickCount:clickCount];
461         [_hostWindow _mouseUpAtPoint:pointInWindow clickCount:clickCount];
462     }
463 }
464
465 - (void)mouseEnterAtPoint:(NSPoint)pointInWindow
466 {
467     [self mouseEntered:[self _mouseEventWithType:NSEventTypeMouseEntered atLocation:pointInWindow]];
468 }
469
470 - (void)mouseDragToPoint:(NSPoint)pointInWindow
471 {
472     [self mouseDragged:[self _mouseEventWithType:NSEventTypeLeftMouseDragged atLocation:pointInWindow]];
473 }
474
475 - (NSEvent *)_mouseEventWithType:(NSEventType)type atLocation:(NSPoint)pointInWindow
476 {
477     return [self _mouseEventWithType:type atLocation:pointInWindow flags:0 timestamp:GetCurrentEventTime() clickCount:0];
478 }
479
480 - (NSEvent *)_mouseEventWithType:(NSEventType)type atLocation:(NSPoint)locationInWindow flags:(NSEventModifierFlags)flags timestamp:(NSTimeInterval)timestamp clickCount:(NSUInteger)clickCount
481 {
482     switch (type) {
483     case NSEventTypeMouseEntered:
484     case NSEventTypeMouseExited:
485         return [NSEvent enterExitEventWithType:type location:locationInWindow modifierFlags:flags timestamp:timestamp windowNumber:[_hostWindow windowNumber] context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber trackingNumber:1 userData:nil];
486     default:
487         return [NSEvent mouseEventWithType:type location:locationInWindow modifierFlags:flags timestamp:timestamp windowNumber:[_hostWindow windowNumber] context:[NSGraphicsContext currentContext] eventNumber:++gEventNumber clickCount:clickCount pressure:0];
488     }
489 }
490
491 - (NSWindow *)hostWindow
492 {
493     return _hostWindow.get();
494 }
495
496 - (void)typeCharacter:(char)character
497 {
498     NSString *characterAsString = [NSString stringWithFormat:@"%c" , character];
499     NSEventType keyDownEventType = NSEventTypeKeyDown;
500     NSEventType keyUpEventType = NSEventTypeKeyUp;
501     [self keyDown:[NSEvent keyEventWithType:keyDownEventType location:NSZeroPoint modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:[_hostWindow windowNumber] context:nil characters:characterAsString charactersIgnoringModifiers:characterAsString isARepeat:NO keyCode:character]];
502     [self keyUp:[NSEvent keyEventWithType:keyUpEventType location:NSZeroPoint modifierFlags:0 timestamp:GetCurrentEventTime() windowNumber:[_hostWindow windowNumber] context:nil characters:characterAsString charactersIgnoringModifiers:characterAsString isARepeat:NO keyCode:character]];
503 }
504
505 @end
506 #endif
507
508 #endif // WK_API_ENABLED