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