[iOS] Add a quirk to synthesize mouse events when modifying the selection
[WebKit-https.git] / LayoutTests / resources / ui-helper.js
1
2 window.UIHelper = class UIHelper {
3     static isIOS()
4     {
5         return navigator.userAgent.includes('iPhone') || navigator.userAgent.includes('iPad');
6     }
7
8     static isWebKit2()
9     {
10         return window.testRunner.isWebKit2;
11     }
12
13     static doubleClickAt(x, y)
14     {
15         eventSender.mouseMoveTo(x, y);
16         eventSender.mouseDown();
17         eventSender.mouseUp();
18         eventSender.mouseDown();
19         eventSender.mouseUp();
20     }
21
22     static doubleClickAtThenDragTo(x1, y1, x2, y2)
23     {
24         eventSender.mouseMoveTo(x1, y1);
25         eventSender.mouseDown();
26         eventSender.mouseUp();
27         eventSender.mouseDown();
28         eventSender.mouseMoveTo(x2, y2);
29         eventSender.mouseUp();
30     }
31
32     static tapAt(x, y, modifiers=[])
33     {
34         console.assert(this.isIOS());
35
36         if (!this.isWebKit2()) {
37             console.assert(!modifiers || !modifiers.length);
38             eventSender.addTouchPoint(x, y);
39             eventSender.touchStart();
40             eventSender.releaseTouchPoint(0);
41             eventSender.touchEnd();
42             return Promise.resolve();
43         }
44
45         return new Promise((resolve) => {
46             testRunner.runUIScript(`
47                 uiController.singleTapAtPointWithModifiers(${x}, ${y}, ${JSON.stringify(modifiers)}, function() {
48                     uiController.uiScriptComplete();
49                 });`, resolve);
50         });
51     }
52
53     static doubleTapAt(x, y)
54     {
55         console.assert(this.isIOS());
56
57         if (!this.isWebKit2()) {
58             eventSender.addTouchPoint(x, y);
59             eventSender.touchStart();
60             eventSender.releaseTouchPoint(0);
61             eventSender.touchEnd();
62             eventSender.addTouchPoint(x, y);
63             eventSender.touchStart();
64             eventSender.releaseTouchPoint(0);
65             eventSender.touchEnd();
66             return Promise.resolve();
67         }
68
69         return new Promise((resolve) => {
70             testRunner.runUIScript(`
71                 uiController.doubleTapAtPoint(${x}, ${y}, function() {
72                     uiController.uiScriptComplete();
73                 });`, resolve);
74         });
75     }
76
77     static humanSpeedDoubleTapAt(x, y)
78     {
79         console.assert(this.isIOS());
80
81         if (!this.isWebKit2()) {
82             // FIXME: Add a sleep in here.
83             eventSender.addTouchPoint(x, y);
84             eventSender.touchStart();
85             eventSender.releaseTouchPoint(0);
86             eventSender.touchEnd();
87             eventSender.addTouchPoint(x, y);
88             eventSender.touchStart();
89             eventSender.releaseTouchPoint(0);
90             eventSender.touchEnd();
91             return Promise.resolve();
92         }
93
94         return new Promise(async (resolve) => {
95             await UIHelper.tapAt(x, y);
96             await new Promise(resolveAfterDelay => setTimeout(resolveAfterDelay, 120));
97             await UIHelper.tapAt(x, y);
98             resolve();
99         });
100     }
101
102     static humanSpeedZoomByDoubleTappingAt(x, y)
103     {
104         console.assert(this.isIOS());
105
106         if (!this.isWebKit2()) {
107             // FIXME: Add a sleep in here.
108             eventSender.addTouchPoint(x, y);
109             eventSender.touchStart();
110             eventSender.releaseTouchPoint(0);
111             eventSender.touchEnd();
112             eventSender.addTouchPoint(x, y);
113             eventSender.touchStart();
114             eventSender.releaseTouchPoint(0);
115             eventSender.touchEnd();
116             return Promise.resolve();
117         }
118
119         return new Promise(async (resolve) => {
120             await UIHelper.tapAt(x, y);
121             await new Promise(resolveAfterDelay => setTimeout(resolveAfterDelay, 120));
122             await new Promise((resolveAfterZoom) => {
123                 testRunner.runUIScript(`
124                     uiController.didEndZoomingCallback = () => {
125                         uiController.didEndZoomingCallback = null;
126                         uiController.uiScriptComplete(uiController.zoomScale);
127                     };
128                     uiController.singleTapAtPoint(${x}, ${y}, () => {});`, resolveAfterZoom);
129             });
130             resolve();
131         });
132     }
133
134     static zoomByDoubleTappingAt(x, y)
135     {
136         console.assert(this.isIOS());
137
138         if (!this.isWebKit2()) {
139             eventSender.addTouchPoint(x, y);
140             eventSender.touchStart();
141             eventSender.releaseTouchPoint(0);
142             eventSender.touchEnd();
143             eventSender.addTouchPoint(x, y);
144             eventSender.touchStart();
145             eventSender.releaseTouchPoint(0);
146             eventSender.touchEnd();
147             return Promise.resolve();
148         }
149
150         return new Promise((resolve) => {
151             testRunner.runUIScript(`
152                 uiController.didEndZoomingCallback = () => {
153                     uiController.didEndZoomingCallback = null;
154                     uiController.uiScriptComplete(uiController.zoomScale);
155                 };
156                 uiController.doubleTapAtPoint(${x}, ${y}, () => {});`, resolve);
157         });
158     }
159
160     static activateAt(x, y)
161     {
162         if (!this.isWebKit2() || !this.isIOS()) {
163             eventSender.mouseMoveTo(x, y);
164             eventSender.mouseDown();
165             eventSender.mouseUp();
166             return Promise.resolve();
167         }
168
169         return new Promise((resolve) => {
170             testRunner.runUIScript(`
171                 uiController.singleTapAtPoint(${x}, ${y}, function() {
172                     uiController.uiScriptComplete();
173                 });`, resolve);
174         });
175     }
176
177     static activateElement(element)
178     {
179         const x = element.offsetLeft + element.offsetWidth / 2;
180         const y = element.offsetTop + element.offsetHeight / 2;
181         return UIHelper.activateAt(x, y);
182     }
183
184     static async doubleActivateAt(x, y)
185     {
186         if (this.isIOS())
187             await UIHelper.doubleTapAt(x, y);
188         else
189             await UIHelper.doubleClickAt(x, y);
190     }
191
192     static async doubleActivateAtSelectionStart()
193     {
194         const rects = window.getSelection().getRangeAt(0).getClientRects();
195         const x = rects[0].left;
196         const y = rects[0].top;
197         if (this.isIOS()) {
198             await UIHelper.activateAndWaitForInputSessionAt(x, y);
199             await UIHelper.doubleTapAt(x, y);
200             // This is only here to deal with async/sync copy/paste calls, so
201             // once <rdar://problem/16207002> is resolved, should be able to remove for faster tests.
202             await new Promise(resolve => testRunner.runUIScript("uiController.uiScriptComplete()", resolve));
203         } else
204             await UIHelper.doubleClickAt(x, y);
205     }
206
207     static async selectWordByDoubleTapOrClick(element, relativeX = 5, relativeY = 5)
208     {
209         const boundingRect = element.getBoundingClientRect();
210         const x = boundingRect.x + relativeX;
211         const y = boundingRect.y + relativeY;
212         if (this.isIOS()) {
213             await UIHelper.activateAndWaitForInputSessionAt(x, y);
214             await UIHelper.doubleTapAt(x, y);
215             // This is only here to deal with async/sync copy/paste calls, so
216             // once <rdar://problem/16207002> is resolved, should be able to remove for faster tests.
217             await new Promise(resolve => testRunner.runUIScript("uiController.uiScriptComplete()", resolve));
218         } else {
219             await UIHelper.doubleClickAt(x, y);
220         }
221     }
222
223     static keyDown(key, modifiers=[])
224     {
225         if (!this.isWebKit2() || !this.isIOS()) {
226             eventSender.keyDown(key, modifiers);
227             return Promise.resolve();
228         }
229
230         return new Promise((resolve) => {
231             testRunner.runUIScript(`uiController.keyDown("${key}", ${JSON.stringify(modifiers)});`, resolve);
232         });
233     }
234
235     static toggleCapsLock()
236     {
237         return new Promise((resolve) => {
238             testRunner.runUIScript(`uiController.toggleCapsLock(() => uiController.uiScriptComplete());`, resolve);
239         });
240     }
241
242     static ensurePresentationUpdate()
243     {
244         if (!this.isWebKit2()) {
245             testRunner.display();
246             return Promise.resolve();
247         }
248
249         return new Promise(resolve => {
250             testRunner.runUIScript(`
251                 uiController.doAfterPresentationUpdate(function() {
252                     uiController.uiScriptComplete();
253                 });`, resolve);
254         });
255     }
256
257     static delayFor(ms)
258     {
259         return new Promise(resolve => setTimeout(resolve, ms));
260     }
261     
262     static immediateScrollTo(x, y)
263     {
264         if (!this.isWebKit2()) {
265             window.scrollTo(x, y);
266             return Promise.resolve();
267         }
268
269         return new Promise(resolve => {
270             testRunner.runUIScript(`
271                 uiController.immediateScrollToOffset(${x}, ${y});`, resolve);
272         });
273     }
274
275     static immediateUnstableScrollTo(x, y)
276     {
277         if (!this.isWebKit2()) {
278             window.scrollTo(x, y);
279             return Promise.resolve();
280         }
281
282         return new Promise(resolve => {
283             testRunner.runUIScript(`
284                 uiController.stableStateOverride = false;
285                 uiController.immediateScrollToOffset(${x}, ${y});`, resolve);
286         });
287     }
288
289     static immediateScrollElementAtContentPointToOffset(x, y, scrollX, scrollY, scrollUpdatesDisabled = false)
290     {
291         if (!this.isWebKit2())
292             return Promise.resolve();
293
294         return new Promise(resolve => {
295             testRunner.runUIScript(`
296                 uiController.scrollUpdatesDisabled = ${scrollUpdatesDisabled};
297                 uiController.immediateScrollElementAtContentPointToOffset(${x}, ${y}, ${scrollX}, ${scrollY});`, resolve);
298         });
299     }
300
301     static ensureVisibleContentRectUpdate()
302     {
303         if (!this.isWebKit2())
304             return Promise.resolve();
305
306         return new Promise(resolve => {
307             const visibleContentRectUpdateScript = "uiController.doAfterVisibleContentRectUpdate(() => uiController.uiScriptComplete())";
308             testRunner.runUIScript(visibleContentRectUpdateScript, resolve);
309         });
310     }
311
312     static activateAndWaitForInputSessionAt(x, y)
313     {
314         if (!this.isWebKit2() || !this.isIOS())
315             return this.activateAt(x, y);
316
317         return new Promise(resolve => {
318             testRunner.runUIScript(`
319                 (function() {
320                     uiController.didShowKeyboardCallback = function() {
321                         uiController.uiScriptComplete();
322                     };
323                     uiController.singleTapAtPoint(${x}, ${y}, function() { });
324                 })()`, resolve);
325         });
326     }
327
328     static activateElementAndWaitForInputSession(element)
329     {
330         const x = element.offsetLeft + element.offsetWidth / 2;
331         const y = element.offsetTop + element.offsetHeight / 2;
332         return this.activateAndWaitForInputSessionAt(x, y);
333     }
334
335     static activateFormControl(element)
336     {
337         if (!this.isWebKit2() || !this.isIOS())
338             return this.activateElement(element);
339
340         const x = element.offsetLeft + element.offsetWidth / 2;
341         const y = element.offsetTop + element.offsetHeight / 2;
342
343         return new Promise(resolve => {
344             testRunner.runUIScript(`
345                 (function() {
346                     uiController.didStartFormControlInteractionCallback = function() {
347                         uiController.uiScriptComplete();
348                     };
349                     uiController.singleTapAtPoint(${x}, ${y}, function() { });
350                 })()`, resolve);
351         });
352     }
353
354     static isShowingKeyboard()
355     {
356         return new Promise(resolve => {
357             testRunner.runUIScript("uiController.isShowingKeyboard", result => resolve(result === "true"));
358         });
359     }
360
361     static isPresentingModally()
362     {
363         return new Promise(resolve => {
364             testRunner.runUIScript("uiController.isPresentingModally", result => resolve(result === "true"));
365         });
366     }
367
368     static deactivateFormControl(element)
369     {
370         if (!this.isWebKit2() || !this.isIOS()) {
371             element.blur();
372             return Promise.resolve();
373         }
374
375         return new Promise(async resolve => {
376             element.blur();
377             while (await this.isPresentingModally())
378                 continue;
379             while (await this.isShowingKeyboard())
380                 continue;
381             resolve();
382         });
383     }
384
385     static waitForPopoverToPresent()
386     {
387         if (!this.isWebKit2() || !this.isIOS())
388             return Promise.resolve();
389
390         return new Promise(resolve => {
391             testRunner.runUIScript(`
392                 (function() {
393                     if (uiController.isShowingPopover)
394                         uiController.uiScriptComplete();
395                     else
396                         uiController.willPresentPopoverCallback = () => uiController.uiScriptComplete();
397                 })()`, resolve);
398         });
399     }
400
401     static waitForPopoverToDismiss()
402     {
403         if (!this.isWebKit2() || !this.isIOS())
404             return Promise.resolve();
405
406         return new Promise(resolve => {
407             testRunner.runUIScript(`
408                 (function() {
409                     if (uiController.isShowingPopover)
410                         uiController.didDismissPopoverCallback = () => uiController.uiScriptComplete();
411                     else
412                         uiController.uiScriptComplete();
413                 })()`, resolve);
414         });
415     }
416
417     static waitForKeyboardToHide()
418     {
419         if (!this.isWebKit2() || !this.isIOS())
420             return Promise.resolve();
421
422         return new Promise(resolve => {
423             testRunner.runUIScript(`
424                 (function() {
425                     if (uiController.isShowingKeyboard)
426                         uiController.didHideKeyboardCallback = () => uiController.uiScriptComplete();
427                     else
428                         uiController.uiScriptComplete();
429                 })()`, resolve);
430         });
431     }
432
433     static getUICaretRect()
434     {
435         if (!this.isWebKit2() || !this.isIOS())
436             return Promise.resolve();
437
438         return new Promise(resolve => {
439             testRunner.runUIScript(`(function() {
440                 uiController.doAfterNextStablePresentationUpdate(function() {
441                     uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
442                 });
443             })()`, jsonString => {
444                 resolve(JSON.parse(jsonString));
445             });
446         });
447     }
448
449     static getUISelectionRects()
450     {
451         if (!this.isWebKit2() || !this.isIOS())
452             return Promise.resolve();
453
454         return new Promise(resolve => {
455             testRunner.runUIScript(`(function() {
456                 uiController.doAfterNextStablePresentationUpdate(function() {
457                     uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionRangeRects));
458                 });
459             })()`, jsonString => {
460                 resolve(JSON.parse(jsonString));
461             });
462         });
463     }
464
465     static getUICaretViewRect()
466     {
467         if (!this.isWebKit2() || !this.isIOS())
468             return Promise.resolve();
469
470         return new Promise(resolve => {
471             testRunner.runUIScript(`(function() {
472                 uiController.doAfterNextStablePresentationUpdate(function() {
473                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionCaretViewRect));
474                 });
475             })()`, jsonString => {
476                 resolve(JSON.parse(jsonString));
477             });
478         });
479     }
480
481     static getUISelectionViewRects()
482     {
483         if (!this.isWebKit2() || !this.isIOS())
484             return Promise.resolve();
485
486         return new Promise(resolve => {
487             testRunner.runUIScript(`(function() {
488                 uiController.doAfterNextStablePresentationUpdate(function() {
489                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
490                 });
491             })()`, jsonString => {
492                 resolve(JSON.parse(jsonString));
493             });
494         });
495     }
496
497     static getSelectionStartGrabberViewRect()
498     {
499         if (!this.isWebKit2() || !this.isIOS())
500             return Promise.resolve();
501
502         return new Promise(resolve => {
503             testRunner.runUIScript(`(function() {
504                 uiController.doAfterNextStablePresentationUpdate(function() {
505                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionStartGrabberViewRect));
506                 });
507             })()`, jsonString => {
508                 resolve(JSON.parse(jsonString));
509             });
510         });
511     }
512
513     static getSelectionEndGrabberViewRect()
514     {
515         if (!this.isWebKit2() || !this.isIOS())
516             return Promise.resolve();
517
518         return new Promise(resolve => {
519             testRunner.runUIScript(`(function() {
520                 uiController.doAfterNextStablePresentationUpdate(function() {
521                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionEndGrabberViewRect));
522                 });
523             })()`, jsonString => {
524                 resolve(JSON.parse(jsonString));
525             });
526         });
527     }
528
529     static replaceTextAtRange(text, location, length) {
530         return new Promise(resolve => {
531             testRunner.runUIScript(`(() => {
532                 uiController.replaceTextAtRange("${text}", ${location}, ${length});
533                 uiController.uiScriptComplete();
534             })()`, resolve);
535         });
536     }
537
538     static wait(promise)
539     {
540         testRunner.waitUntilDone();
541         if (window.finishJSTest)
542             window.jsTestIsAsync = true;
543
544         let finish = () => {
545             if (window.finishJSTest)
546                 finishJSTest();
547             else
548                 testRunner.notifyDone();
549         }
550
551         return promise.then(finish, finish);
552     }
553
554     static withUserGesture(callback)
555     {
556         internals.withUserGesture(callback);
557     }
558
559     static selectFormAccessoryPickerRow(rowIndex)
560     {
561         const selectRowScript = `(() => uiController.selectFormAccessoryPickerRow(${rowIndex}))()`;
562         return new Promise(resolve => testRunner.runUIScript(selectRowScript, resolve));
563     }
564
565     static selectFormPopoverTitle()
566     {
567         return new Promise(resolve => {
568             testRunner.runUIScript(`(() => {
569                 uiController.uiScriptComplete(uiController.selectFormPopoverTitle);
570             })()`, resolve);
571         });
572     }
573
574     static enterText(text)
575     {
576         const escapedText = text.replace(/`/g, "\\`");
577         const enterTextScript = `(() => uiController.enterText(\`${escapedText}\`))()`;
578         return new Promise(resolve => testRunner.runUIScript(enterTextScript, resolve));
579     }
580
581     static setTimePickerValue(hours, minutes)
582     {
583         const setValueScript = `(() => uiController.setTimePickerValue(${hours}, ${minutes}))()`;
584         return new Promise(resolve => testRunner.runUIScript(setValueScript, resolve));
585     }
586
587     static setShareSheetCompletesImmediatelyWithResolution(resolved)
588     {
589         const resolveShareSheet = `(() => uiController.setShareSheetCompletesImmediatelyWithResolution(${resolved}))()`;
590         return new Promise(resolve => testRunner.runUIScript(resolveShareSheet, resolve));
591     }
592
593     static textContentType()
594     {
595         return new Promise(resolve => {
596             testRunner.runUIScript(`(() => {
597                 uiController.uiScriptComplete(uiController.textContentType);
598             })()`, resolve);
599         });
600     }
601
602     static formInputLabel()
603     {
604         return new Promise(resolve => {
605             testRunner.runUIScript(`(() => {
606                 uiController.uiScriptComplete(uiController.formInputLabel);
607             })()`, resolve);
608         });
609     }
610
611     static isShowingDataListSuggestions()
612     {
613         return new Promise(resolve => {
614             testRunner.runUIScript(`(() => {
615                 uiController.uiScriptComplete(uiController.isShowingDataListSuggestions);
616             })()`, result => resolve(result === "true"));
617         });
618     }
619
620     static zoomScale()
621     {
622         return new Promise(resolve => {
623             testRunner.runUIScript(`(() => {
624                 uiController.uiScriptComplete(uiController.zoomScale);
625             })()`, scaleAsString => resolve(parseFloat(scaleAsString)));
626         });
627     }
628
629     static zoomToScale(scale)
630     {
631         const uiScript = `uiController.zoomToScale(${scale}, () => uiController.uiScriptComplete(uiController.zoomScale))`;
632         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
633     }
634
635     static typeCharacter(characterString)
636     {
637         if (!this.isWebKit2() || !this.isIOS()) {
638             eventSender.keyDown(characterString);
639             return;
640         }
641
642         const escapedString = characterString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
643         const uiScript = `uiController.typeCharacterUsingHardwareKeyboard(\`${escapedString}\`, () => uiController.uiScriptComplete())`;
644         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
645     }
646
647     static applyAutocorrection(newText, oldText)
648     {
649         if (!this.isWebKit2())
650             return;
651
652         const [escapedNewText, escapedOldText] = [newText.replace(/`/g, "\\`"), oldText.replace(/`/g, "\\`")];
653         const uiScript = `uiController.applyAutocorrection(\`${escapedNewText}\`, \`${escapedOldText}\`, () => uiController.uiScriptComplete())`;
654         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
655     }
656
657     static inputViewBounds()
658     {
659         if (!this.isWebKit2() || !this.isIOS())
660             return Promise.resolve();
661
662         return new Promise(resolve => {
663             testRunner.runUIScript(`(() => {
664                 uiController.uiScriptComplete(JSON.stringify(uiController.inputViewBounds));
665             })()`, jsonString => {
666                 resolve(JSON.parse(jsonString));
667             });
668         });
669     }
670
671     static calendarType()
672     {
673         if (!this.isWebKit2())
674             return Promise.resolve();
675
676         return new Promise(resolve => {
677             testRunner.runUIScript(`(() => {
678                 uiController.doAfterNextStablePresentationUpdate(() => {
679                     uiController.uiScriptComplete(JSON.stringify(uiController.calendarType));
680                 })
681             })()`, jsonString => {
682                 resolve(JSON.parse(jsonString));
683             });
684         });
685     }
686
687     static setDefaultCalendarType(calendarIdentifier)
688     {
689         if (!this.isWebKit2())
690             return Promise.resolve();
691
692         return new Promise(resolve => testRunner.runUIScript(`uiController.setDefaultCalendarType('${calendarIdentifier}')`, resolve));
693
694     }
695
696     static setViewScale(scale)
697     {
698         if (!this.isWebKit2())
699             return Promise.resolve();
700
701         return new Promise(resolve => testRunner.runUIScript(`uiController.setViewScale(${scale})`, resolve));
702     }
703
704     static resignFirstResponder()
705     {
706         if (!this.isWebKit2())
707             return Promise.resolve();
708
709         return new Promise(resolve => testRunner.runUIScript(`uiController.resignFirstResponder()`, resolve));
710     }
711
712     static minimumZoomScale()
713     {
714         if (!this.isWebKit2())
715             return Promise.resolve();
716
717         return new Promise(resolve => {
718             testRunner.runUIScript(`(() => {
719                 uiController.uiScriptComplete(uiController.minimumZoomScale);
720             })()`, scaleAsString => resolve(parseFloat(scaleAsString)))
721         });
722     }
723
724     static drawSquareInEditableImage()
725     {
726         if (!this.isWebKit2())
727             return Promise.resolve();
728
729         return new Promise(resolve => testRunner.runUIScript(`uiController.drawSquareInEditableImage()`, resolve));
730     }
731
732     static stylusTapAt(x, y, modifiers=[])
733     {
734         if (!this.isWebKit2())
735             return Promise.resolve();
736
737         return new Promise((resolve) => {
738             testRunner.runUIScript(`
739                 uiController.stylusTapAtPointWithModifiers(${x}, ${y}, 2, 1, 0.5, ${JSON.stringify(modifiers)}, function() {
740                     uiController.uiScriptComplete();
741                 });`, resolve);
742         });
743     }
744
745     static numberOfStrokesInEditableImage()
746     {
747         if (!this.isWebKit2())
748             return Promise.resolve();
749
750         return new Promise(resolve => {
751             testRunner.runUIScript(`(() => {
752                 uiController.uiScriptComplete(uiController.numberOfStrokesInEditableImage);
753             })()`, numberAsString => resolve(parseInt(numberAsString, 10)))
754         });
755     }
756
757     static attachmentInfo(attachmentIdentifier)
758     {
759         if (!this.isWebKit2())
760             return Promise.resolve();
761
762         return new Promise(resolve => {
763             testRunner.runUIScript(`(() => {
764                 uiController.uiScriptComplete(JSON.stringify(uiController.attachmentInfo('${attachmentIdentifier}')));
765             })()`, jsonString => {
766                 resolve(JSON.parse(jsonString));
767             })
768         });
769     }
770
771     static setMinimumEffectiveWidth(effectiveWidth)
772     {
773         if (!this.isWebKit2())
774             return Promise.resolve();
775
776         return new Promise(resolve => testRunner.runUIScript(`uiController.setMinimumEffectiveWidth(${effectiveWidth})`, resolve));
777     }
778
779     static setAllowsViewportShrinkToFit(allows)
780     {
781         if (!this.isWebKit2())
782             return Promise.resolve();
783
784         return new Promise(resolve => testRunner.runUIScript(`uiController.setAllowsViewportShrinkToFit(${allows})`, resolve));
785     }
786
787     static setKeyboardInputModeIdentifier(identifier)
788     {
789         if (!this.isWebKit2())
790             return Promise.resolve();
791
792         const escapedIdentifier = identifier.replace(/`/g, "\\`");
793         return new Promise(resolve => testRunner.runUIScript(`uiController.setKeyboardInputModeIdentifier(\`${escapedIdentifier}\`)`, resolve));
794     }
795
796     static contentOffset()
797     {
798         if (!this.isIOS())
799             return Promise.resolve();
800
801         const uiScript = "JSON.stringify([uiController.contentOffsetX, uiController.contentOffsetY])";
802         return new Promise(resolve => testRunner.runUIScript(uiScript, result => {
803             const [offsetX, offsetY] = JSON.parse(result)
804             resolve({ x: offsetX, y: offsetY });
805         }));
806     }
807
808     static undoAndRedoLabels()
809     {
810         if (!this.isWebKit2())
811             return Promise.resolve();
812
813         const script = "JSON.stringify([uiController.lastUndoLabel, uiController.firstRedoLabel])";
814         return new Promise(resolve => testRunner.runUIScript(script, result => resolve(JSON.parse(result))));
815     }
816
817     static waitForMenuToShow()
818     {
819         return new Promise(resolve => {
820             testRunner.runUIScript(`
821                 (function() {
822                     if (!uiController.isShowingMenu)
823                         uiController.didShowMenuCallback = () => uiController.uiScriptComplete();
824                     else
825                         uiController.uiScriptComplete();
826                 })()`, resolve);
827         });
828     }
829
830     static waitForMenuToHide()
831     {
832         return new Promise(resolve => {
833             testRunner.runUIScript(`
834                 (function() {
835                     if (uiController.isShowingMenu)
836                         uiController.didHideMenuCallback = () => uiController.uiScriptComplete();
837                     else
838                         uiController.uiScriptComplete();
839                 })()`, resolve);
840         });
841     }
842
843     static isShowingMenu()
844     {
845         return new Promise(resolve => {
846             testRunner.runUIScript(`uiController.isShowingMenu`, result => resolve(result === "true"));
847         });
848     }
849
850     static isDismissingMenu()
851     {
852         return new Promise(resolve => {
853             testRunner.runUIScript(`uiController.isDismissingMenu`, result => resolve(result === "true"));
854         });
855     }
856
857     static menuRect()
858     {
859         return new Promise(resolve => {
860             testRunner.runUIScript("JSON.stringify(uiController.menuRect)", result => resolve(JSON.parse(result)));
861         });
862     }
863
864     static setHardwareKeyboardAttached(attached)
865     {
866         return new Promise(resolve => testRunner.runUIScript(`uiController.setHardwareKeyboardAttached(${attached ? "true" : "false"})`, resolve));
867     }
868
869     static rectForMenuAction(action)
870     {
871         return new Promise(resolve => {
872             testRunner.runUIScript(`
873                 const rect = uiController.rectForMenuAction("${action}");
874                 uiController.uiScriptComplete(rect ? JSON.stringify(rect) : "");
875             `, stringResult => {
876                 resolve(stringResult.length ? JSON.parse(stringResult) : null);
877             });
878         });
879     }
880
881     static async chooseMenuAction(action)
882     {
883         const menuRect = await this.rectForMenuAction(action);
884         if (menuRect)
885             await this.activateAt(menuRect.left + menuRect.width / 2, menuRect.top + menuRect.height / 2);
886     }
887 }