13834285e736b402932d508187eb6f95037bd8c5
[WebKit-https.git] / Tools / WebKitTestRunner / ios / UIScriptControllerIOS.mm
1 /*
2  * Copyright (C) 2015-2019 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_FAMILY)
30
31 #import "HIDEventGenerator.h"
32 #import "PencilKitTestSPI.h"
33 #import "PlatformWebView.h"
34 #import "StringFunctions.h"
35 #import "TestController.h"
36 #import "TestRunnerWKWebView.h"
37 #import "UIKitSPI.h"
38 #import "UIScriptContext.h"
39 #import <JavaScriptCore/JavaScriptCore.h>
40 #import <JavaScriptCore/OpaqueJSString.h>
41 #import <UIKit/UIKit.h>
42 #import <WebCore/FloatRect.h>
43 #import <WebKit/WKWebViewPrivate.h>
44 #import <WebKit/WebKit.h>
45 #import <wtf/SoftLinking.h>
46 #import <wtf/Vector.h>
47
48 SOFT_LINK_FRAMEWORK(UIKit)
49 SOFT_LINK_CLASS(UIKit, UIPhysicalKeyboardEvent)
50
51 @interface UIPhysicalKeyboardEvent (UIPhysicalKeyboardEventHack)
52 @property (nonatomic, assign) NSInteger _modifierFlags;
53 @end
54
55 namespace WTR {
56
57 static NSDictionary *toNSDictionary(CGRect rect)
58 {
59     return @{
60         @"left": @(rect.origin.x),
61         @"top": @(rect.origin.y),
62         @"width": @(rect.size.width),
63         @"height": @(rect.size.height)
64     };
65 }
66
67 static unsigned arrayLength(JSContextRef context, JSObjectRef array)
68 {
69     auto lengthString = adopt(JSStringCreateWithUTF8CString("length"));
70     if (auto lengthValue = JSObjectGetProperty(context, array, lengthString.get(), nullptr))
71         return static_cast<unsigned>(JSValueToNumber(context, lengthValue, nullptr));
72     return 0;
73 }
74
75 static Vector<String> parseModifierArray(JSContextRef context, JSValueRef arrayValue)
76 {
77     if (!arrayValue)
78         return { };
79
80     // The value may either be a string with a single modifier or an array of modifiers.
81     if (JSValueIsString(context, arrayValue)) {
82         auto string = toWTFString(toWK(adopt(JSValueToStringCopy(context, arrayValue, nullptr))));
83         return { string };
84     }
85
86     if (!JSValueIsObject(context, arrayValue))
87         return { };
88     JSObjectRef array = const_cast<JSObjectRef>(arrayValue);
89     unsigned length = arrayLength(context, array);
90     Vector<String> modifiers;
91     modifiers.reserveInitialCapacity(length);
92     for (unsigned i = 0; i < length; ++i) {
93         JSValueRef exception = nullptr;
94         JSValueRef value = JSObjectGetPropertyAtIndex(context, array, i, &exception);
95         if (exception)
96             continue;
97         auto string = adopt(JSValueToStringCopy(context, value, &exception));
98         if (exception)
99             continue;
100         modifiers.append(toWTFString(toWK(string.get())));
101     }
102     return modifiers;
103 }
104
105 void UIScriptController::checkForOutstandingCallbacks()
106 {
107     if (![[HIDEventGenerator sharedHIDEventGenerator] checkForOutstandingCallbacks])
108         [NSException raise:@"WebKitTestRunnerTestProblem" format:@"The test completed before all synthesized events had been handled. Perhaps you're calling notifyDone() too early?"];
109 }
110
111 void UIScriptController::doAfterPresentationUpdate(JSValueRef callback)
112 {
113     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
114
115     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
116     [webView _doAfterNextPresentationUpdate:^{
117         if (!m_context)
118             return;
119         m_context->asyncTaskComplete(callbackID);
120     }];
121 }
122
123 void UIScriptController::doAfterNextStablePresentationUpdate(JSValueRef callback)
124 {
125     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
126
127     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
128     [webView _doAfterNextStablePresentationUpdate:^() {
129         if (m_context)
130             m_context->asyncTaskComplete(callbackID);
131     }];
132 }
133
134 void UIScriptController::doAfterVisibleContentRectUpdate(JSValueRef callback)
135 {
136     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
137
138     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
139     [webView _doAfterNextVisibleContentRectUpdate:^ {
140         if (!m_context)
141             return;
142         m_context->asyncTaskComplete(callbackID);
143     }];
144 }
145
146 void UIScriptController::zoomToScale(double scale, JSValueRef callback)
147 {
148     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
149
150     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
151
152     [webView zoomToScale:scale animated:YES completionHandler:^{
153         if (!m_context)
154             return;
155         m_context->asyncTaskComplete(callbackID);
156     }];
157 }
158
159 void UIScriptController::retrieveSpeakSelectionContent(JSValueRef callback)
160 {
161     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
162     
163     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
164     
165     [webView accessibilityRetrieveSpeakSelectionContentWithCompletionHandler:^() {
166         if (!m_context)
167             return;
168         m_context->asyncTaskComplete(callbackID);
169     }];
170 }
171
172 JSRetainPtr<JSStringRef> UIScriptController::accessibilitySpeakSelectionContent() const
173 {
174     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
175     return JSStringCreateWithCFString((CFStringRef)webView.accessibilitySpeakSelectionContent);
176 }
177
178 void UIScriptController::simulateAccessibilitySettingsChangeNotification(JSValueRef callback)
179 {
180     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
181
182     auto* webView = TestController::singleton().mainWebView()->platformView();
183     NSNotificationCenter *center = [NSNotificationCenter defaultCenter];
184     [center postNotificationName:UIAccessibilityInvertColorsStatusDidChangeNotification object:webView];
185
186     [webView _doAfterNextPresentationUpdate: ^{
187         if (!m_context)
188             return;
189         m_context->asyncTaskComplete(callbackID);
190     }];
191 }
192
193 double UIScriptController::zoomScale() const
194 {
195     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
196     return webView.scrollView.zoomScale;
197 }
198
199 static CGPoint globalToContentCoordinates(TestRunnerWKWebView *webView, long x, long y)
200 {
201     CGPoint point = CGPointMake(x, y);
202     point = [webView _convertPointFromContentsToView:point];
203     point = [webView convertPoint:point toView:nil];
204     point = [webView.window convertPoint:point toWindow:nil];
205     return point;
206 }
207
208 void UIScriptController::touchDownAtPoint(long x, long y, long touchCount, JSValueRef callback)
209 {
210     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
211
212     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
213     [[HIDEventGenerator sharedHIDEventGenerator] touchDown:location touchCount:touchCount completionBlock:^{
214         if (!m_context)
215             return;
216         m_context->asyncTaskComplete(callbackID);
217     }];
218 }
219
220 void UIScriptController::liftUpAtPoint(long x, long y, long touchCount, JSValueRef callback)
221 {
222     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
223     
224     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
225     [[HIDEventGenerator sharedHIDEventGenerator] liftUp:location touchCount:touchCount completionBlock:^{
226         if (!m_context)
227             return;
228         m_context->asyncTaskComplete(callbackID);
229     }];
230 }
231
232 void UIScriptController::singleTapAtPoint(long x, long y, JSValueRef callback)
233 {
234     singleTapAtPointWithModifiers(x, y, nullptr, callback);
235 }
236
237 void UIScriptController::singleTapAtPointWithModifiers(long x, long y, JSValueRef modifierArray, JSValueRef callback)
238 {
239     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
240
241     auto modifierFlags = parseModifierArray(m_context->jsContext(), modifierArray);
242     for (auto& modifierFlag : modifierFlags)
243         [[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag];
244
245     [[HIDEventGenerator sharedHIDEventGenerator] tap:globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y) completionBlock:^{
246         if (!m_context)
247             return;
248         for (size_t i = modifierFlags.size(); i; ) {
249             --i;
250             [[HIDEventGenerator sharedHIDEventGenerator] keyUp:modifierFlags[i]];
251         }
252         [[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:^{
253             if (!m_context)
254                 return;
255             m_context->asyncTaskComplete(callbackID);
256         }];
257     }];
258 }
259
260 void UIScriptController::doubleTapAtPoint(long x, long y, JSValueRef callback)
261 {
262     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
263
264     [[HIDEventGenerator sharedHIDEventGenerator] doubleTap:globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y) completionBlock:^{
265         if (!m_context)
266             return;
267         m_context->asyncTaskComplete(callbackID);
268     }];
269 }
270
271 void UIScriptController::stylusDownAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
272 {
273     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
274
275     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
276     [[HIDEventGenerator sharedHIDEventGenerator] stylusDownAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:^{
277         if (!m_context)
278             return;
279         m_context->asyncTaskComplete(callbackID);
280     }];
281 }
282
283 void UIScriptController::stylusMoveToPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
284 {
285     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
286
287     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
288     [[HIDEventGenerator sharedHIDEventGenerator] stylusMoveToPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:^{
289         if (!m_context)
290             return;
291         m_context->asyncTaskComplete(callbackID);
292     }];
293 }
294
295 void UIScriptController::stylusUpAtPoint(long x, long y, JSValueRef callback)
296 {
297     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
298
299     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
300     [[HIDEventGenerator sharedHIDEventGenerator] stylusUpAtPoint:location completionBlock:^{
301         if (!m_context)
302             return;
303         m_context->asyncTaskComplete(callbackID);
304     }];
305 }
306
307 void UIScriptController::stylusTapAtPoint(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef callback)
308 {
309     stylusTapAtPointWithModifiers(x, y, azimuthAngle, altitudeAngle, pressure, nullptr, callback);
310 }
311
312 void UIScriptController::stylusTapAtPointWithModifiers(long x, long y, float azimuthAngle, float altitudeAngle, float pressure, JSValueRef modifierArray, JSValueRef callback)
313 {
314     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
315
316     auto modifierFlags = parseModifierArray(m_context->jsContext(), modifierArray);
317     for (auto& modifierFlag : modifierFlags)
318         [[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag];
319
320     auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y);
321     [[HIDEventGenerator sharedHIDEventGenerator] stylusTapAtPoint:location azimuthAngle:azimuthAngle altitudeAngle:altitudeAngle pressure:pressure completionBlock:^{
322         if (!m_context)
323             return;
324         for (size_t i = modifierFlags.size(); i; ) {
325             --i;
326             [[HIDEventGenerator sharedHIDEventGenerator] keyUp:modifierFlags[i]];
327         }
328         [[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:^{
329             if (!m_context)
330                 return;
331             m_context->asyncTaskComplete(callbackID);
332         }];
333     }];
334 }
335
336 void convertCoordinates(NSMutableDictionary *event)
337 {
338     if (event[HIDEventTouchesKey]) {
339         for (NSMutableDictionary *touch in event[HIDEventTouchesKey]) {
340             auto location = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), (long)[touch[HIDEventXKey] doubleValue], (long)[touch[HIDEventYKey]doubleValue]);
341             touch[HIDEventXKey] = @(location.x);
342             touch[HIDEventYKey] = @(location.y);
343         }
344     }
345 }
346
347 void UIScriptController::sendEventStream(JSStringRef eventsJSON, JSValueRef callback)
348 {
349     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
350
351     String jsonString = eventsJSON->string();
352     auto eventInfo = dynamic_objc_cast<NSDictionary>([NSJSONSerialization JSONObjectWithData:[(NSString *)jsonString dataUsingEncoding:NSUTF8StringEncoding] options:NSJSONReadingMutableContainers | NSJSONReadingMutableLeaves error:nil]);
353     
354     for (NSMutableDictionary *event in eventInfo[TopLevelEventInfoKey]) {
355         if (![event[HIDEventCoordinateSpaceKey] isEqualToString:HIDEventCoordinateSpaceTypeContent])
356             continue;
357         
358         if (event[HIDEventStartEventKey])
359             convertCoordinates(event[HIDEventStartEventKey]);
360         
361         if (event[HIDEventEndEventKey])
362             convertCoordinates(event[HIDEventEndEventKey]);
363         
364         if (event[HIDEventTouchesKey])
365             convertCoordinates(event);
366     }
367     
368     if (!eventInfo || ![eventInfo isKindOfClass:[NSDictionary class]]) {
369         WTFLogAlways("JSON is not convertible to a dictionary");
370         return;
371     }
372     
373     [[HIDEventGenerator sharedHIDEventGenerator] sendEventStream:eventInfo completionBlock:^{
374         if (!m_context)
375             return;
376         m_context->asyncTaskComplete(callbackID);
377     }];
378 }
379
380 void UIScriptController::dragFromPointToPoint(long startX, long startY, long endX, long endY, double durationSeconds, JSValueRef callback)
381 {
382     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
383
384     CGPoint startPoint = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), startX, startY);
385     CGPoint endPoint = globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), endX, endY);
386     
387     [[HIDEventGenerator sharedHIDEventGenerator] dragWithStartPoint:startPoint endPoint:endPoint duration:durationSeconds completionBlock:^{
388         if (!m_context)
389             return;
390         m_context->asyncTaskComplete(callbackID);
391     }];
392 }
393     
394 void UIScriptController::longPressAtPoint(long x, long y, JSValueRef callback)
395 {
396     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
397     
398     [[HIDEventGenerator sharedHIDEventGenerator] longPress:globalToContentCoordinates(TestController::singleton().mainWebView()->platformView(), x, y) completionBlock:^{
399         if (!m_context)
400             return;
401         m_context->asyncTaskComplete(callbackID);
402     }];
403 }
404
405 void UIScriptController::enterText(JSStringRef text)
406 {
407     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
408     auto textAsCFString = adoptCF(JSStringCopyCFString(kCFAllocatorDefault, text));
409     [webView _simulateTextEntered:(NSString *)textAsCFString.get()];
410 }
411
412 void UIScriptController::typeCharacterUsingHardwareKeyboard(JSStringRef character, JSValueRef callback)
413 {
414     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
415
416     // Assumes that the keyboard is already shown.
417     [[HIDEventGenerator sharedHIDEventGenerator] keyPress:toWTFString(toWK(character)) completionBlock:^{
418         if (!m_context)
419             return;
420         m_context->asyncTaskComplete(callbackID);
421     }];
422 }
423
424 static UIPhysicalKeyboardEvent *createUIPhysicalKeyboardEvent(NSString *hidInputString, NSString *uiEventInputString, UIKeyModifierFlags modifierFlags, UIKeyboardInputFlags inputFlags, bool isKeyDown)
425 {
426     auto* keyboardEvent = [getUIPhysicalKeyboardEventClass() _eventWithInput:uiEventInputString inputFlags:inputFlags];
427     keyboardEvent._modifierFlags = modifierFlags;
428     auto hidEvent = createHIDKeyEvent(hidInputString, keyboardEvent.timestamp, isKeyDown);
429     [keyboardEvent _setHIDEvent:hidEvent.get() keyboard:nullptr];
430     return keyboardEvent;
431 }
432
433 void UIScriptController::keyDown(JSStringRef character, JSValueRef modifierArray)
434 {
435     // Character can be either a single Unicode code point or the name of a special key (e.g. "downArrow").
436     // HIDEventGenerator knows how to map these special keys to the appropriate keycode.
437     String inputString = toWTFString(toWK(character));
438     auto modifierFlags = parseModifierArray(m_context->jsContext(), modifierArray);
439
440     for (auto& modifierFlag : modifierFlags)
441         [[HIDEventGenerator sharedHIDEventGenerator] keyDown:modifierFlag];
442
443     [[HIDEventGenerator sharedHIDEventGenerator] keyDown:inputString];
444     [[HIDEventGenerator sharedHIDEventGenerator] keyUp:inputString];
445
446     for (size_t i = modifierFlags.size(); i; ) {
447         --i;
448         [[HIDEventGenerator sharedHIDEventGenerator] keyUp:modifierFlags[i]];
449     }
450
451     [[HIDEventGenerator sharedHIDEventGenerator] sendMarkerHIDEventWithCompletionBlock:^{ /* Do nothing */ }];
452 }
453
454 void UIScriptController::dismissFormAccessoryView()
455 {
456     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
457     [webView dismissFormAccessoryView];
458 }
459
460 JSRetainPtr<JSStringRef> UIScriptController::selectFormPopoverTitle() const
461 {
462     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
463     return JSStringCreateWithCFString((CFStringRef)webView.selectFormPopoverTitle);
464 }
465
466 JSRetainPtr<JSStringRef> UIScriptController::textContentType() const
467 {
468     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
469     return JSStringCreateWithCFString((CFStringRef)(webView.textContentTypeForTesting ?: @""));
470 }
471
472 JSRetainPtr<JSStringRef> UIScriptController::formInputLabel() const
473 {
474     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
475     return JSStringCreateWithCFString((CFStringRef)webView.formInputLabel);
476 }
477
478 void UIScriptController::selectFormAccessoryPickerRow(long rowIndex)
479 {
480     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
481     [webView selectFormAccessoryPickerRow:rowIndex];
482 }
483
484 void UIScriptController::setTimePickerValue(long hour, long minute)
485 {
486     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
487     [webView setTimePickerValueToHour:hour minute:minute];
488 }
489
490 bool UIScriptController::isPresentingModally() const
491 {
492     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
493     return !!webView.window.rootViewController.presentedViewController;
494 }
495
496 static CGPoint contentOffsetBoundedInValidRange(UIScrollView *scrollView, CGPoint contentOffset)
497 {
498     UIEdgeInsets contentInsets = scrollView.contentInset;
499     CGSize contentSize = scrollView.contentSize;
500     CGSize scrollViewSize = scrollView.bounds.size;
501
502     CGFloat maxHorizontalOffset = contentSize.width + contentInsets.right - scrollViewSize.width;
503     contentOffset.x = std::min(maxHorizontalOffset, contentOffset.x);
504     contentOffset.x = std::max(-contentInsets.left, contentOffset.x);
505
506     CGFloat maxVerticalOffset = contentSize.height + contentInsets.bottom - scrollViewSize.height;
507     contentOffset.y = std::min(maxVerticalOffset, contentOffset.y);
508     contentOffset.y = std::max(-contentInsets.top, contentOffset.y);
509     return contentOffset;
510 }
511
512 double UIScriptController::contentOffsetX() const
513 {
514     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
515     return webView.scrollView.contentOffset.x;
516 }
517
518 double UIScriptController::contentOffsetY() const
519 {
520     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
521     return webView.scrollView.contentOffset.y;
522 }
523
524 void UIScriptController::scrollToOffset(long x, long y)
525 {
526     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
527     [webView.scrollView setContentOffset:contentOffsetBoundedInValidRange(webView.scrollView, CGPointMake(x, y)) animated:YES];
528 }
529
530 void UIScriptController::immediateScrollToOffset(long x, long y)
531 {
532     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
533     [webView.scrollView setContentOffset:contentOffsetBoundedInValidRange(webView.scrollView, CGPointMake(x, y)) animated:NO];
534 }
535
536 void UIScriptController::immediateZoomToScale(double scale)
537 {
538     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
539     [webView.scrollView setZoomScale:scale animated:NO];
540 }
541
542 void UIScriptController::keyboardAccessoryBarNext()
543 {
544     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
545     [webView keyboardAccessoryBarNext];
546 }
547
548 void UIScriptController::keyboardAccessoryBarPrevious()
549 {
550     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
551     [webView keyboardAccessoryBarPrevious];
552 }
553
554 bool UIScriptController::isShowingKeyboard() const
555 {
556     return TestController::singleton().mainWebView()->platformView().showingKeyboard;
557 }
558
559 void UIScriptController::applyAutocorrection(JSStringRef newString, JSStringRef oldString, JSValueRef callback)
560 {
561     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
562
563     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
564     [webView applyAutocorrection:toWTFString(toWK(newString)) toString:toWTFString(toWK(oldString)) withCompletionHandler:^ {
565         // applyAutocorrection can call its completion handler synchronously,
566         // which makes UIScriptController unhappy (see bug 172884).
567         dispatch_async(dispatch_get_main_queue(), ^ {
568             if (!m_context)
569                 return;
570             m_context->asyncTaskComplete(callbackID);
571         });
572     }];
573 }
574
575 double UIScriptController::minimumZoomScale() const
576 {
577     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
578     return webView.scrollView.minimumZoomScale;
579 }
580
581 double UIScriptController::maximumZoomScale() const
582 {
583     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
584     return webView.scrollView.maximumZoomScale;
585 }
586
587 Optional<bool> UIScriptController::stableStateOverride() const
588 {
589     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
590     if (webView._stableStateOverride)
591         return webView._stableStateOverride.boolValue;
592
593     return WTF::nullopt;
594 }
595
596 void UIScriptController::setStableStateOverride(Optional<bool> overrideValue)
597 {
598     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
599     if (overrideValue)
600         webView._stableStateOverride = @(overrideValue.value());
601     else
602         webView._stableStateOverride = nil;
603 }
604
605 JSObjectRef UIScriptController::contentVisibleRect() const
606 {
607     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
608
609     CGRect contentVisibleRect = webView._contentVisibleRect;
610     
611     WebCore::FloatRect rect(contentVisibleRect.origin.x, contentVisibleRect.origin.y, contentVisibleRect.size.width, contentVisibleRect.size.height);
612     return m_context->objectFromRect(rect);
613 }
614
615 JSObjectRef UIScriptController::textSelectionRangeRects() const
616 {
617     auto selectionRects = adoptNS([[NSMutableArray alloc] init]);
618     NSArray *rects = TestController::singleton().mainWebView()->platformView()._uiTextSelectionRects;
619     for (NSValue *rect in rects)
620         [selectionRects addObject:toNSDictionary(rect.CGRectValue)];
621
622     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:selectionRects.get() inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
623 }
624
625 JSObjectRef UIScriptController::textSelectionCaretRect() const
626 {
627     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(TestController::singleton().mainWebView()->platformView()._uiTextCaretRect) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
628 }
629
630 JSObjectRef UIScriptController::selectionStartGrabberViewRect() const
631 {
632     WKWebView *webView = TestController::singleton().mainWebView()->platformView();
633     UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
634     UIView *selectionRangeView = [contentView valueForKeyPath:@"interactionAssistant.selectionView.rangeView"];
635     auto frameInContentCoordinates = [selectionRangeView convertRect:[[selectionRangeView valueForKeyPath:@"startGrabber"] frame] toView:contentView];
636     frameInContentCoordinates = CGRectIntersection(contentView.bounds, frameInContentCoordinates);
637     auto jsContext = m_context->jsContext();
638     return JSValueToObject(jsContext, [JSValue valueWithObject:toNSDictionary(frameInContentCoordinates) inContext:[JSContext contextWithJSGlobalContextRef:jsContext]].JSValueRef, nullptr);
639 }
640
641 JSObjectRef UIScriptController::selectionEndGrabberViewRect() const
642 {
643     WKWebView *webView = TestController::singleton().mainWebView()->platformView();
644     UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
645     UIView *selectionRangeView = [contentView valueForKeyPath:@"interactionAssistant.selectionView.rangeView"];
646     auto frameInContentCoordinates = [selectionRangeView convertRect:[[selectionRangeView valueForKeyPath:@"endGrabber"] frame] toView:contentView];
647     frameInContentCoordinates = CGRectIntersection(contentView.bounds, frameInContentCoordinates);
648     auto jsContext = m_context->jsContext();
649     return JSValueToObject(jsContext, [JSValue valueWithObject:toNSDictionary(frameInContentCoordinates) inContext:[JSContext contextWithJSGlobalContextRef:jsContext]].JSValueRef, nullptr);
650 }
651
652 JSObjectRef UIScriptController::selectionCaretViewRect() const
653 {
654     WKWebView *webView = TestController::singleton().mainWebView()->platformView();
655     UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
656     UIView *caretView = [contentView valueForKeyPath:@"interactionAssistant.selectionView.caretView"];
657     auto rectInContentViewCoordinates = CGRectIntersection([caretView convertRect:caretView.bounds toView:contentView], contentView.bounds);
658     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(rectInContentViewCoordinates) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
659 }
660
661 JSObjectRef UIScriptController::selectionRangeViewRects() const
662 {
663     WKWebView *webView = TestController::singleton().mainWebView()->platformView();
664     UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
665     UIView *rangeView = [contentView valueForKeyPath:@"interactionAssistant.selectionView.rangeView"];
666     auto rectsAsDictionaries = adoptNS([[NSMutableArray alloc] init]);
667     NSArray *textRectInfoArray = [rangeView valueForKeyPath:@"rects"];
668     for (id textRectInfo in textRectInfoArray) {
669         NSValue *rectValue = [textRectInfo valueForKeyPath:@"rect"];
670         auto rangeRectInContentViewCoordinates = [rangeView convertRect:rectValue.CGRectValue toView:contentView];
671         [rectsAsDictionaries addObject:toNSDictionary(CGRectIntersection(rangeRectInContentViewCoordinates, contentView.bounds))];
672     }
673     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:rectsAsDictionaries.get() inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
674 }
675
676 JSObjectRef UIScriptController::inputViewBounds() const
677 {
678     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:toNSDictionary(TestController::singleton().mainWebView()->platformView()._inputViewBounds) inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
679 }
680
681 void UIScriptController::removeAllDynamicDictionaries()
682 {
683     [UIKeyboard removeAllDynamicDictionaries];
684 }
685
686 JSRetainPtr<JSStringRef> UIScriptController::scrollingTreeAsText() const
687 {
688     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
689     return JSStringCreateWithCFString((CFStringRef)[webView _scrollingTreeAsText]);
690 }
691
692 JSObjectRef UIScriptController::propertiesOfLayerWithID(uint64_t layerID) const
693 {
694     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:[TestController::singleton().mainWebView()->platformView() _propertiesOfLayerWithID:layerID] inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
695 }
696
697 static UIDeviceOrientation toUIDeviceOrientation(DeviceOrientation* orientation)
698 {
699     if (!orientation)
700         return UIDeviceOrientationPortrait;
701         
702     switch (*orientation) {
703     case DeviceOrientation::Portrait:
704         return UIDeviceOrientationPortrait;
705     case DeviceOrientation::PortraitUpsideDown:
706         return UIDeviceOrientationPortraitUpsideDown;
707     case DeviceOrientation::LandscapeLeft:
708         return UIDeviceOrientationLandscapeLeft;
709     case DeviceOrientation::LandscapeRight:
710         return UIDeviceOrientationLandscapeRight;
711     }
712     
713     return UIDeviceOrientationPortrait;
714 }
715
716 void UIScriptController::simulateRotation(DeviceOrientation* orientation, JSValueRef callback)
717 {
718     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
719     webView.usesSafariLikeRotation = NO;
720     
721     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
722     
723     webView.rotationDidEndCallback = ^{
724         if (!m_context)
725             return;
726         m_context->asyncTaskComplete(callbackID);
727     };
728     
729     [[UIDevice currentDevice] setOrientation:toUIDeviceOrientation(orientation) animated:YES];
730 }
731
732 void UIScriptController::simulateRotationLikeSafari(DeviceOrientation* orientation, JSValueRef callback)
733 {
734     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
735     webView.usesSafariLikeRotation = YES;
736     
737     unsigned callbackID = m_context->prepareForAsyncTask(callback, CallbackTypeNonPersistent);
738     
739     webView.rotationDidEndCallback = ^{
740         if (!m_context)
741             return;
742         m_context->asyncTaskComplete(callbackID);
743     };
744     
745     [[UIDevice currentDevice] setOrientation:toUIDeviceOrientation(orientation) animated:YES];
746 }
747
748 void UIScriptController::platformSetDidStartFormControlInteractionCallback()
749 {
750     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
751     webView.didStartFormControlInteractionCallback = ^{
752         if (!m_context)
753             return;
754         m_context->fireCallback(CallbackTypeDidStartFormControlInteraction);
755     };
756 }
757
758 void UIScriptController::platformSetDidEndFormControlInteractionCallback()
759 {
760     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
761     webView.didEndFormControlInteractionCallback = ^{
762         if (!m_context)
763             return;
764         m_context->fireCallback(CallbackTypeDidEndFormControlInteraction);
765     };
766 }
767     
768 void UIScriptController::platformSetDidShowForcePressPreviewCallback()
769 {
770     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
771     webView.didShowForcePressPreviewCallback = ^ {
772         if (!m_context)
773             return;
774         m_context->fireCallback(CallbackTypeDidShowForcePressPreview);
775     };
776 }
777
778 void UIScriptController::platformSetDidDismissForcePressPreviewCallback()
779 {
780     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
781     webView.didDismissForcePressPreviewCallback = ^ {
782         if (!m_context)
783             return;
784         m_context->fireCallback(CallbackTypeDidEndFormControlInteraction);
785     };
786 }
787
788 void UIScriptController::platformSetWillBeginZoomingCallback()
789 {
790     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
791     webView.willBeginZoomingCallback = ^{
792         if (!m_context)
793             return;
794         m_context->fireCallback(CallbackTypeWillBeginZooming);
795     };
796 }
797
798 void UIScriptController::platformSetDidEndZoomingCallback()
799 {
800     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
801     webView.didEndZoomingCallback = ^{
802         if (!m_context)
803             return;
804         m_context->fireCallback(CallbackTypeDidEndZooming);
805     };
806 }
807
808 void UIScriptController::platformSetDidShowKeyboardCallback()
809 {
810     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
811     webView.didShowKeyboardCallback = ^{
812         if (!m_context)
813             return;
814         m_context->fireCallback(CallbackTypeDidShowKeyboard);
815     };
816 }
817
818 void UIScriptController::platformSetDidHideKeyboardCallback()
819 {
820     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
821     webView.didHideKeyboardCallback = ^{
822         if (!m_context)
823             return;
824         m_context->fireCallback(CallbackTypeDidHideKeyboard);
825     };
826 }
827
828 void UIScriptController::platformSetDidEndScrollingCallback()
829 {
830     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
831     webView.didEndScrollingCallback = ^{
832         if (!m_context)
833             return;
834         m_context->fireCallback(CallbackTypeDidEndScrolling);
835     };
836 }
837
838 void UIScriptController::platformClearAllCallbacks()
839 {
840     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
841     
842     webView.didStartFormControlInteractionCallback = nil;
843     webView.didEndFormControlInteractionCallback = nil;
844     webView.didShowForcePressPreviewCallback = nil;
845     webView.didDismissForcePressPreviewCallback = nil;
846     webView.didEndZoomingCallback = nil;
847     webView.willBeginZoomingCallback = nil;
848     webView.didHideKeyboardCallback = nil;
849     webView.didShowKeyboardCallback = nil;
850     webView.didEndScrollingCallback = nil;
851     webView.rotationDidEndCallback = nil;
852 }
853
854 void UIScriptController::setSafeAreaInsets(double top, double right, double bottom, double left)
855 {
856     UIEdgeInsets insets = UIEdgeInsetsMake(top, left, bottom, right);
857     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
858     webView.overrideSafeAreaInsets = insets;
859 }
860
861 void UIScriptController::beginBackSwipe(JSValueRef callback)
862 {
863     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
864     [webView _beginBackSwipeForTesting];
865 }
866
867 void UIScriptController::completeBackSwipe(JSValueRef callback)
868 {
869     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
870     [webView _completeBackSwipeForTesting];
871 }
872
873 static BOOL forEachViewInHierarchy(UIView *view, void(^mapFunction)(UIView *subview, BOOL *stop))
874 {
875     BOOL stop = NO;
876     mapFunction(view, &stop);
877     if (stop)
878         return YES;
879
880     for (UIView *subview in view.subviews) {
881         stop = forEachViewInHierarchy(subview, mapFunction);
882         if (stop)
883             break;
884     }
885     return stop;
886 }
887
888 bool UIScriptController::isShowingDataListSuggestions() const
889 {
890     Class remoteKeyboardWindowClass = NSClassFromString(@"UIRemoteKeyboardWindow");
891     Class suggestionsPickerViewClass = NSClassFromString(@"WKDataListSuggestionsPickerView");
892     UIWindow *remoteInputHostingWindow = nil;
893     for (UIWindow *window in UIApplication.sharedApplication.windows) {
894         if ([window isKindOfClass:remoteKeyboardWindowClass])
895             remoteInputHostingWindow = window;
896     }
897
898     if (!remoteInputHostingWindow)
899         return false;
900
901     __block bool foundDataListSuggestionsPickerView = false;
902     forEachViewInHierarchy(remoteInputHostingWindow, ^(UIView *subview, BOOL *stop) {
903         if (![subview isKindOfClass:suggestionsPickerViewClass])
904             return;
905
906         foundDataListSuggestionsPickerView = true;
907         *stop = YES;
908     });
909     return foundDataListSuggestionsPickerView;
910 }
911
912 #if HAVE(PENCILKIT)
913 static PKCanvasView *findEditableImageCanvas()
914 {
915     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
916     Class pkCanvasViewClass = NSClassFromString(@"PKCanvasView");
917     __block PKCanvasView *canvasView = nil;
918     forEachViewInHierarchy(webView.window, ^(UIView *subview, BOOL *stop) {
919         if (![subview isKindOfClass:pkCanvasViewClass])
920             return;
921
922         canvasView = (PKCanvasView *)subview;
923         *stop = YES;
924     });
925     return canvasView;
926 }
927 #endif
928
929 void UIScriptController::drawSquareInEditableImage()
930 {
931 #if HAVE(PENCILKIT)
932     Class pkDrawingClass = NSClassFromString(@"PKDrawing");
933     Class pkInkClass = NSClassFromString(@"PKInk");
934     Class pkStrokeClass = NSClassFromString(@"PKStroke");
935
936     PKCanvasView *canvasView = findEditableImageCanvas();
937     RetainPtr<PKDrawing> drawing = canvasView.drawing ?: adoptNS([[pkDrawingClass alloc] init]);
938     RetainPtr<CGPathRef> path = adoptCF(CGPathCreateWithRect(CGRectMake(0, 0, 50, 50), NULL));
939     RetainPtr<PKInk> ink = [pkInkClass inkWithType:0 color:UIColor.greenColor weight:100.0];
940     RetainPtr<PKStroke> stroke = adoptNS([[pkStrokeClass alloc] _initWithPath:path.get() ink:ink.get() inputScale:1]);
941     [drawing _addStroke:stroke.get()];
942
943     [canvasView setDrawing:drawing.get()];
944 #endif
945 }
946
947 long UIScriptController::numberOfStrokesInEditableImage()
948 {
949 #if HAVE(PENCILKIT)
950     PKCanvasView *canvasView = findEditableImageCanvas();
951     return canvasView.drawing._allStrokes.count;
952 #else
953     return 0;
954 #endif
955 }
956
957 void UIScriptController::setKeyboardInputModeIdentifier(JSStringRef identifier)
958 {
959     TestController::singleton().setKeyboardInputModeIdentifier(toWTFString(toWK(identifier)));
960 }
961
962 // FIXME: Write this in terms of HIDEventGenerator once we know how to reset caps lock state
963 // on test completion to avoid it effecting subsequent tests.
964 void UIScriptController::toggleCapsLock(JSValueRef callback)
965 {
966     m_capsLockOn = !m_capsLockOn;
967     auto *keyboardEvent = createUIPhysicalKeyboardEvent(@"capsLock", [NSString string], m_capsLockOn ? UIKeyModifierAlphaShift : 0,
968         kUIKeyboardInputModifierFlagsChanged, m_capsLockOn);
969     [[UIApplication sharedApplication] handleKeyUIEvent:keyboardEvent];
970     doAsyncTask(callback);
971 }
972
973 JSObjectRef UIScriptController::attachmentInfo(JSStringRef jsAttachmentIdentifier)
974 {
975     TestRunnerWKWebView *webView = TestController::singleton().mainWebView()->platformView();
976
977     auto attachmentIdentifier = toWTFString(toWK(jsAttachmentIdentifier));
978     _WKAttachment *attachment = [webView _attachmentForIdentifier:attachmentIdentifier];
979     _WKAttachmentInfo *attachmentInfo = attachment.info;
980
981     NSDictionary *attachmentInfoDictionary = @{
982         @"id": attachmentIdentifier,
983         @"name": attachmentInfo.name,
984         @"contentType": attachmentInfo.contentType,
985         @"filePath": attachmentInfo.filePath,
986         @"size": @(attachmentInfo.data.length),
987     };
988
989     return JSValueToObject(m_context->jsContext(), [JSValue valueWithObject:attachmentInfoDictionary inContext:[JSContext contextWithJSGlobalContextRef:m_context->jsContext()]].JSValueRef, nullptr);
990 }
991
992 NSUndoManager *UIScriptController::platformUndoManager() const
993 {
994     return [(UIView *)[TestController::singleton().mainWebView()->platformView() valueForKeyPath:@"_currentContentView"] undoManager];
995 }
996
997 JSObjectRef UIScriptController::calendarType() const
998 {
999     WKWebView *webView = TestController::singleton().mainWebView()->platformView();
1000     UIView *contentView = [webView valueForKeyPath:@"_currentContentView"];
1001     NSString *calendarTypeString = [contentView valueForKeyPath:@"formInputControl.dateTimePickerCalendarType"];
1002     auto jsContext = m_context->jsContext();
1003     return JSValueToObject(jsContext, [JSValue valueWithObject:calendarTypeString inContext:[JSContext contextWithJSGlobalContextRef:jsContext]].JSValueRef, nullptr);
1004 }
1005
1006 }
1007
1008 #endif // PLATFORM(IOS_FAMILY)