994b6da7c80f1be6321989ec2f42d7b63cc7b121
[WebKit-https.git] / Tools / WebKitTestRunner / ios / UIScriptControllerIOS.mm
1 /*
2  * Copyright (C) 2015 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 "UIScriptController.h"
28
29 #if PLATFORM(IOS)
30
31 #import "HIDEventGenerator.h"
32 #import "PlatformWebView.h"
33 #import "StringFunctions.h"
34 #import "TestController.h"
35 #import "TestRunnerWKWebView.h"
36 #import "UIKitSPI.h"
37 #import "UIScriptContext.h"
38 #import <JavaScriptCore/JavaScriptCore.h>
39 #import <JavaScriptCore/OpaqueJSString.h>
40 #import <UIKit/UIKit.h>
41 #import <WebCore/FloatRect.h>
42 #import <WebKit/WKWebViewPrivate.h>
43 #import <WebKit/WebKit.h>
44
45 namespace WTR {
46
47 void UIScriptController::doAsyncTask(JSValueRef callback)
48 {
49     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
50
51     dispatch_async(dispatch_get_main_queue(), ^{
52         if (!m_context)
53             return;
54         m_context->asyncTaskComplete(callbackID);
55     });
56 }
57
58 void UIScriptController::zoomToScale(double scale, JSValueRef callback)
59 {
60     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
61
62     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
63
64     [webView zoomToScale:scale animated:YES completionHandler:^{
65         if (!m_context)
66             return;
67         m_context->asyncTaskComplete(callbackID);
68     }];
69 }
70
71 void UIScriptController::simulateAccessibilitySettingsChangeNotification(JSValueRef callback)
72 {
73     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
74
75     auto* webView = TestController::singleton().mainWebView()->platformView();
76     NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
77     [center postNotificationName:UIAccessibilityInvertColorsStatusDidChangeNotification object:webView];
78
79     [webView _doAfterNextPresentationUpdate: ^{
80         if (!m_context)
81             return;
82         m_context->asyncTaskComplete(callbackID);
83     }];
84 }
85
86 double UIScriptController::zoomScale() const
87 {
88     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
89     return webView.scrollView.zoomScale;
90 }
91
92 static CGPoint globalToContentCoordinates(TestRunnerWKWebView *webView, long x, long y)
93 {
94     CGPoint point = CGPointMake(x, y);
95     point = [webView _convertPointFromContentsToView:point];
96     point = [webView convertPoint:point toView:nil];
97     point = [webView.window convertPoint:point toWindow:nil];
98     return point;
99 }
100
101 void UIScriptController::touchDownAtPoint(long x, long y, long touchCount, JSValueRef callback)
102 {
103     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
104
105     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
106     [[HIDEventGenerator sharedHIDEventGenerator] touchDown:location touchCount:touchCount completionBlock:^{
107         if (!m_context)
108             return;
109         m_context->asyncTaskComplete(callbackID);
110     }];
111 }
112
113 void UIScriptController::liftUpAtPoint(long x, long y, long touchCount, JSValueRef callback)
114 {
115     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
116     
117     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
118     [[HIDEventGenerator sharedHIDEventGenerator] liftUp:location touchCount:touchCount completionBlock:^{
119         if (!m_context)
120             return;
121         m_context->asyncTaskComplete(callbackID);
122     }];
123 }
124
125 void UIScriptController::singleTapAtPoint(long x, long y, JSValueRef callback)
126 {
127     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
128
129     [[HIDEventGenerator sharedHIDEventGenerator] tap:globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y) completionBlock:^{
130         if (!m_context)
131             return;
132         m_context->asyncTaskComplete(callbackID);
133     }];
134 }
135
136 void UIScriptController::doubleTapAtPoint(long x, long y, JSValueRef callback)
137 {
138     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
139
140     [[HIDEventGenerator sharedHIDEventGenerator] doubleTap:globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y) completionBlock:^{
141         if (!m_context)
142             return;
143         m_context->asyncTaskComplete(callbackID);
144     }];
145 }
146
147 void UIScriptController::stylusDownAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
148 {
149     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
150
151     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
152     [[HIDEventGenerator sharedHIDEventGenerator] stylusDownAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:^{
153         if (!m_context)
154             return;
155         m_context->asyncTaskComplete(callbackID);
156     }];
157 }
158
159 void UIScriptController::stylusMoveToPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
160 {
161     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
162
163     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
164     [[HIDEventGenerator sharedHIDEventGenerator] stylusMoveToPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:^{
165         if (!m_context)
166             return;
167         m_context->asyncTaskComplete(callbackID);
168     }];
169 }
170
171 void UIScriptController::stylusUpAtPoint(long x, long y, JSValueRef callback)
172 {
173     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
174
175     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
176     [[HIDEventGenerator sharedHIDEventGenerator] stylusUpAtPoint:location completionBlock:^{
177         if (!m_context)
178             return;
179         m_context->asyncTaskComplete(callbackID);
180     }];
181 }
182
183 void UIScriptController::stylusTapAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
184 {
185     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
186
187     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
188     [[HIDEventGenerator sharedHIDEventGenerator] stylusTapAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:^{
189         if (!m_context)
190             return;
191         m_context->asyncTaskComplete(callbackID);
192     }];
193 }
194
195 void UIScriptController::sendEventStream(JSStringRef eventsJSON, JSValueRef callback)
196 {
197     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
198
199     String jsonString = eventsJSON->string();
200     auto eventInfo = dynamic_objc_cast<NSDictionary>([NSJSONSerialization JSONObjectWithData:[(NSString *)jsonString dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]);
201     if (!eventInfo || ![eventInfo isKindOfClass:[NSDictionary class]]) {
202         WTFLogAlways("JSON is not convertible to a dictionary");
203         return;
204     }
205     
206     [[HIDEventGenerator sharedHIDEventGenerator] sendEventStream:eventInfo completionBlock:^{
207         if (!m_context)
208             return;
209         m_context->asyncTaskComplete(callbackID);
210     }];
211 }
212
213 void UIScriptController::dragFromPointToPoint(long startX, long startY, long endX, long endY, double durationSeconds, JSValueRef callback)
214 {
215     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
216
217     CGPoint startPoint = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), startX, startY);
218     CGPoint endPoint = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), endX, endY);
219     
220     [[HIDEventGenerator sharedHIDEventGenerator] dragWithStartPoint:startPoint endPoint:endPoint duration:durationSeconds completionBlock:^{
221         if (!m_context)
222             return;
223         m_context->asyncTaskComplete(callbackID);
224     }];
225 }
226     
227 void UIScriptController::longPressAtPoint(long x, long y, JSValueRef callback)
228 {
229     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
230     
231     [[HIDEventGenerator sharedHIDEventGenerator] longPress:globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y) completionBlock:^{
232         if (!m_context)
233             return;
234         m_context->asyncTaskComplete(callbackID);
235     }];
236 }
237
238 void UIScriptController::typeCharacterUsingHardwareKeyboard(JSStringRef character, JSValueRef callback)
239 {
240     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
241
242     // Assumes that the keyboard is already shown.
243     [[HIDEventGenerator sharedHIDEventGenerator] keyPress:toWTFString(toWK(character)) completionBlock:^{
244         if (!m_context)
245             return;
246         m_context->asyncTaskComplete(callbackID);
247     }];
248 }
249
250 void UIScriptController::keyDownUsingHardwareKeyboard(JSStringRef character, JSValueRef callback)
251 {
252     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
253
254     // Assumes that the keyboard is already shown.
255     [[HIDEventGenerator sharedHIDEventGenerator] keyDown:toWTFString(toWK(character)) completionBlock:^{
256         if (!m_context)
257             return;
258         m_context->asyncTaskComplete(callbackID);
259     }];
260 }
261
262 void UIScriptController::keyUpUsingHardwareKeyboard(JSStringRef character, JSValueRef callback)
263 {
264     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
265
266     // Assumes that the keyboard is already shown.
267     [[HIDEventGenerator sharedHIDEventGenerator] keyUp:toWTFString(toWK(character)) completionBlock:^{
268         if (!m_context)
269             return;
270         m_context->asyncTaskComplete(callbackID);
271     }];
272 }
273
274 void UIScriptController::selectTextCandidateAtIndex(long index, JSValueRef callback)
275 {
276 #if USE(APPLE_INTERNAL_SDK)
277     static const float textPredictionsPollingInterval = 0.1;
278     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
279     waitForTextPredictionsViewAndSelectCandidateAtIndex(index, callbackID, textPredictionsPollingInterval);
280 #else
281     // FIXME: This is a no-op on non-internal builds due to UIKeyboardPredictionView being unavailable. Ideally, there should be a better way to
282     // retrieve information and interact with the predictive text view that will be compatible with OpenSource.
283     UNUSED_PARAM(index);
284     UNUSED_PARAM(callback);
285 #endif
286 }
287
288 void UIScriptController::waitForTextPredictionsViewAndSelectCandidateAtIndex(long index, unsigned callbackID, float interval)
289 {
290     id UIKeyboardPredictionViewClass = NSClassFromString(@"UIKeyboardPredictionView");
291     if (!UIKeyboardPredictionViewClass)
292         return;
293
294 #if USE(APPLE_INTERNAL_SDK)
295     UIKeyboardPredictionView *predictionView = (UIKeyboardPredictionView *)[UIKeyboardPredictionViewClass activeInstance];
296     if (![predictionView hasPredictions]) {
297         dispatch_after(dispatch_time(DISPATCH_TIME_NOW, interval * NSEC_PER_SEC), dispatch_get_main_queue(), ^() {
298             waitForTextPredictionsViewAndSelectCandidateAtIndex(index, callbackID, interval);
299         });
300         return;
301     }
302
303     PlatformWKView webView = TestController::singleton().mainWebView()->platformView();
304     CGRect predictionViewFrame = [predictionView frame];
305     // This assumes there are 3 predicted text cells of equal width, which is the case on iOS.
306     float offsetX = (index * 2 + 1) * CGRectGetWidth(predictionViewFrame) / 6;
307     float offsetY = CGRectGetHeight(webView.window.frame) - CGRectGetHeight([[predictionView superview] frame]) + CGRectGetHeight(predictionViewFrame) / 2;
308     [[HIDEventGenerator sharedHIDEventGenerator] tap:CGPointMake(offsetX, offsetY) completionBlock:^{
309         if (m_context)
310             m_context->asyncTaskComplete(callbackID);
311     }];
312 #else
313     UNUSED_PARAM(index);
314     UNUSED_PARAM(callbackID);
315     UNUSED_PARAM(interval);
316 #endif
317 }
318
319 void UIScriptController::dismissFormAccessoryView()
320 {
321     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
322     [webView dismissFormAccessoryView];
323 }
324
325 void UIScriptController::selectFormAccessoryPickerRow(long rowIndex)
326 {
327     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
328     [webView selectFormAccessoryPickerRow:rowIndex];
329 }
330     
331 JSObjectRef UIScriptController::contentsOfUserInterfaceItem(JSStringRef interfaceItem) const
332 {
333     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
334     NSDictionary *contentDictionary = [webView _contentsOfUserInterfaceItem:toWTFString(toWK(interfaceItem))];
335     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:contentDictionary inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
336 }
337
338 static CGPoint contentOffsetBoundedInValidRange(UIScrollView *scrollView, CGPoint contentOffset)
339 {
340     UIEdgeInsets contentInsets = scrollView.contentInset;
341     CGSize contentSize = scrollView.contentSize;
342     CGSize scrollViewSize = scrollView.bounds.size;
343
344     CGFloat maxHorizontalOffset = contentSize.width + contentInsets.right - scrollViewSize.width;
345     contentOffset.x = std::min(maxHorizontalOffset, contentOffset.x);
346     contentOffset.x = std::max(-contentInsets.left, contentOffset.x);
347
348     CGFloat maxVerticalOffset = contentSize.height + contentInsets.bottom - scrollViewSize.height;
349     contentOffset.y = std::min(maxVerticalOffset, contentOffset.y);
350     contentOffset.y = std::max(-contentInsets.top, contentOffset.y);
351     return contentOffset;
352 }
353
354 void UIScriptController::scrollToOffset(long x, long y)
355 {
356     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
357     [webView.scrollView setContentOffset:contentOffsetBoundedInValidRange(webView.scrollView, CGPointMake(x, y)) animated:YES];
358 }
359
360 void UIScriptController::keyboardAccessoryBarNext()
361 {
362     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
363     [webView keyboardAccessoryBarNext];
364 }
365
366 void UIScriptController::keyboardAccessoryBarPrevious()
367 {
368     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
369     [webView keyboardAccessoryBarPrevious];
370 }
371
372 double UIScriptController::minimumZoomScale() const
373 {
374     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
375     return webView.scrollView.minimumZoomScale;
376 }
377
378 double UIScriptController::maximumZoomScale() const
379 {
380     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
381     return webView.scrollView.maximumZoomScale;
382 }
383
384 JSObjectRef UIScriptController::contentVisibleRect() const
385 {
386     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
387
388     CGRect contentVisibleRect = webView._contentVisibleRect;
389     
390     WebCore::FloatRect rect(contentVisibleRect.origin.x, contentVisibleRect.origin.y, contentVisibleRect.size.width, contentVisibleRect.size.height);
391     return m_context->objectFromRect(rect);
392 }
393
394 JSObjectRef UIScriptController::selectionRangeViewRects() const
395 {
396     NSMutableArray *selectionRects = [[NSMutableArray alloc] init];
397     for (UIView *rectView in TestController::singleton().mainWebView()->platformView()._uiTextSelectionRectViews) {
398         if (rectView.hidden)
399             continue;
400
401         CGRect frame = rectView.frame;
402         [selectionRects addObject:@{
403             @"left": @(frame.origin.x),
404             @"top": @(frame.origin.y),
405             @"width": @(frame.size.width),
406             @"height": @(frame.size.height),
407         }];
408     }
409     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:selectionRects inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
410 }
411
412 void UIScriptController::removeAllDynamicDictionaries()
413 {
414     [UIKeyboard removeAllDynamicDictionaries];
415 }
416
417 JSRetainPtr<JSStringRef> UIScriptController::scrollingTreeAsText() const
418 {
419     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
420     return JSStringCreateWithCFString((CFStringRef)[webView _scrollingTreeAsText]);
421 }
422
423 void UIScriptController::platformSetDidStartFormControlInteractionCallback()
424 {
425     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
426     webView.didStartFormControlInteractionCallback = ^{
427         if (!m_context)
428             return;
429         m_context->fireCallback(CallbackTypeDidStartFormControlInteraction);
430     };
431 }
432
433 void UIScriptController::platformSetDidEndFormControlInteractionCallback()
434 {
435     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
436     webView.didEndFormControlInteractionCallback = ^{
437         if (!m_context)
438             return;
439         m_context->fireCallback(CallbackTypeDidEndFormControlInteraction);
440     };
441 }
442     
443 void UIScriptController::platformSetDidShowForcePressPreviewCallback()
444 {
445     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
446     webView.didShowForcePressPreviewCallback = ^ {
447         if (!m_context)
448             return;
449         m_context->fireCallback(CallbackTypeDidShowForcePressPreview);
450     };
451 }
452
453 void UIScriptController::platformSetDidDismissForcePressPreviewCallback()
454 {
455     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
456     webView.didDismissForcePressPreviewCallback = ^ {
457         if (!m_context)
458             return;
459         m_context->fireCallback(CallbackTypeDidEndFormControlInteraction);
460     };
461 }
462
463 void UIScriptController::platformSetWillBeginZoomingCallback()
464 {
465     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
466     webView.willBeginZoomingCallback = ^{
467         if (!m_context)
468             return;
469         m_context->fireCallback(CallbackTypeWillBeginZooming);
470     };
471 }
472
473 void UIScriptController::platformSetDidEndZoomingCallback()
474 {
475     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
476     webView.didEndZoomingCallback = ^{
477         if (!m_context)
478             return;
479         m_context->fireCallback(CallbackTypeDidEndZooming);
480     };
481 }
482
483 void UIScriptController::platformSetDidShowKeyboardCallback()
484 {
485     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
486     webView.didShowKeyboardCallback = ^{
487         if (!m_context)
488             return;
489         m_context->fireCallback(CallbackTypeDidShowKeyboard);
490     };
491 }
492
493 void UIScriptController::platformSetDidHideKeyboardCallback()
494 {
495     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
496     webView.didHideKeyboardCallback = ^{
497         if (!m_context)
498             return;
499         m_context->fireCallback(CallbackTypeDidHideKeyboard);
500     };
501 }
502
503 void UIScriptController::platformSetDidEndScrollingCallback()
504 {
505     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
506     webView.didEndScrollingCallback = ^{
507         if (!m_context)
508             return;
509         m_context->fireCallback(CallbackTypeDidEndScrolling);
510     };
511 }
512
513 void UIScriptController::platformClearAllCallbacks()
514 {
515     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
516     
517     webView.didStartFormControlInteractionCallback = nil;
518     webView.didEndFormControlInteractionCallback = nil;
519     webView.didShowForcePressPreviewCallback = nil;
520     webView.didDismissForcePressPreviewCallback = nil;
521     webView.didEndZoomingCallback = nil;
522     webView.willBeginZoomingCallback = nil;
523     webView.didHideKeyboardCallback = nil;
524     webView.didShowKeyboardCallback = nil;
525     webView.didEndScrollingCallback = nil;
526 }
527
528 }
529
530 #endif // PLATFORM(IOS)