[iOS WK2] A top fixed bar can flicker when scrolling with the keyboard up
[WebKit-https.git] / LayoutTests / resources / ui-helper.js
1
2 window.UIHelper = class UIHelper {
3     static isIOSFamily()
4     {
5         return testRunner.isIOSFamily;
6     }
7
8     static isWebKit2()
9     {
10         return 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.isIOSFamily());
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.isIOSFamily());
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.isIOSFamily());
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.isIOSFamily());
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.isIOSFamily());
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.isIOSFamily()) {
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.isIOSFamily())
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.isIOSFamily()) {
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.isIOSFamily()) {
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.isIOSFamily()) {
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 ensureStablePresentationUpdate()
258     {
259         if (!this.isWebKit2()) {
260             testRunner.display();
261             return Promise.resolve();
262         }
263
264         return new Promise(resolve => {
265             testRunner.runUIScript(`
266                 uiController.doAfterNextStablePresentationUpdate(function() {
267                     uiController.uiScriptComplete();
268                 });`, resolve);
269         });
270     }
271
272     static ensurePositionInformationUpdateForElement(element)
273     {
274         const boundingRect = element.getBoundingClientRect();
275         const x = boundingRect.x + 5;
276         const y = boundingRect.y + 5;
277
278         if (!this.isWebKit2()) {
279             testRunner.display();
280             return Promise.resolve();
281         }
282
283         return new Promise(resolve => {
284             testRunner.runUIScript(`
285                 uiController.ensurePositionInformationIsUpToDateAt(${x}, ${y}, function () {
286                     uiController.uiScriptComplete();
287                 });`, resolve);
288         });
289     }
290
291     static delayFor(ms)
292     {
293         return new Promise(resolve => setTimeout(resolve, ms));
294     }
295     
296     static immediateScrollTo(x, y)
297     {
298         if (!this.isWebKit2()) {
299             window.scrollTo(x, y);
300             return Promise.resolve();
301         }
302
303         return new Promise(resolve => {
304             testRunner.runUIScript(`
305                 uiController.immediateScrollToOffset(${x}, ${y});`, resolve);
306         });
307     }
308
309     static immediateUnstableScrollTo(x, y)
310     {
311         if (!this.isWebKit2()) {
312             window.scrollTo(x, y);
313             return Promise.resolve();
314         }
315
316         return new Promise(resolve => {
317             testRunner.runUIScript(`
318                 uiController.stableStateOverride = false;
319                 uiController.immediateScrollToOffset(${x}, ${y});`, resolve);
320         });
321     }
322
323     static immediateScrollElementAtContentPointToOffset(x, y, scrollX, scrollY, scrollUpdatesDisabled = false)
324     {
325         if (!this.isWebKit2())
326             return Promise.resolve();
327
328         return new Promise(resolve => {
329             testRunner.runUIScript(`
330                 uiController.scrollUpdatesDisabled = ${scrollUpdatesDisabled};
331                 uiController.immediateScrollElementAtContentPointToOffset(${x}, ${y}, ${scrollX}, ${scrollY});`, resolve);
332         });
333     }
334
335     static ensureVisibleContentRectUpdate()
336     {
337         if (!this.isWebKit2())
338             return Promise.resolve();
339
340         return new Promise(resolve => {
341             const visibleContentRectUpdateScript = "uiController.doAfterVisibleContentRectUpdate(() => uiController.uiScriptComplete())";
342             testRunner.runUIScript(visibleContentRectUpdateScript, resolve);
343         });
344     }
345
346     static activateAndWaitForInputSessionAt(x, y)
347     {
348         if (!this.isWebKit2() || !this.isIOSFamily())
349             return this.activateAt(x, y);
350
351         return new Promise(resolve => {
352             testRunner.runUIScript(`
353                 (function() {
354                     uiController.didShowKeyboardCallback = function() {
355                         uiController.uiScriptComplete();
356                     };
357                     uiController.singleTapAtPoint(${x}, ${y}, function() { });
358                 })()`, resolve);
359         });
360     }
361
362     static activateElementAndWaitForInputSession(element)
363     {
364         const x = element.offsetLeft + element.offsetWidth / 2;
365         const y = element.offsetTop + element.offsetHeight / 2;
366         return this.activateAndWaitForInputSessionAt(x, y);
367     }
368
369     static activateFormControl(element)
370     {
371         if (!this.isWebKit2() || !this.isIOSFamily())
372             return this.activateElement(element);
373
374         const x = element.offsetLeft + element.offsetWidth / 2;
375         const y = element.offsetTop + element.offsetHeight / 2;
376
377         return new Promise(resolve => {
378             testRunner.runUIScript(`
379                 (function() {
380                     uiController.didStartFormControlInteractionCallback = function() {
381                         uiController.uiScriptComplete();
382                     };
383                     uiController.singleTapAtPoint(${x}, ${y}, function() { });
384                 })()`, resolve);
385         });
386     }
387
388     static dismissFormAccessoryView()
389     {
390         if (!this.isWebKit2() || !this.isIOSFamily())
391             return Promise.resolve();
392
393         return new Promise(resolve => {
394             testRunner.runUIScript(`
395                 (function() {
396                     uiController.dismissFormAccessoryView();
397                     uiController.uiScriptComplete();
398                 })()`, resolve);
399         });
400     }
401
402     static isShowingKeyboard()
403     {
404         return new Promise(resolve => {
405             testRunner.runUIScript("uiController.isShowingKeyboard", result => resolve(result === "true"));
406         });
407     }
408
409     static hasInputSession()
410     {
411         return new Promise(resolve => {
412             testRunner.runUIScript("uiController.hasInputSession", result => resolve(result === "true"));
413         });
414     }
415
416     static isPresentingModally()
417     {
418         return new Promise(resolve => {
419             testRunner.runUIScript("uiController.isPresentingModally", result => resolve(result === "true"));
420         });
421     }
422
423     static deactivateFormControl(element)
424     {
425         if (!this.isWebKit2() || !this.isIOSFamily()) {
426             element.blur();
427             return Promise.resolve();
428         }
429
430         return new Promise(async resolve => {
431             element.blur();
432             while (await this.isPresentingModally())
433                 continue;
434             while (await this.isShowingKeyboard())
435                 continue;
436             resolve();
437         });
438     }
439
440     static waitForPopoverToPresent()
441     {
442         if (!this.isWebKit2() || !this.isIOSFamily())
443             return Promise.resolve();
444
445         return new Promise(resolve => {
446             testRunner.runUIScript(`
447                 (function() {
448                     if (uiController.isShowingPopover)
449                         uiController.uiScriptComplete();
450                     else
451                         uiController.willPresentPopoverCallback = () => uiController.uiScriptComplete();
452                 })()`, resolve);
453         });
454     }
455
456     static waitForPopoverToDismiss()
457     {
458         if (!this.isWebKit2() || !this.isIOSFamily())
459             return Promise.resolve();
460
461         return new Promise(resolve => {
462             testRunner.runUIScript(`
463                 (function() {
464                     if (uiController.isShowingPopover)
465                         uiController.didDismissPopoverCallback = () => uiController.uiScriptComplete();
466                     else
467                         uiController.uiScriptComplete();
468                 })()`, resolve);
469         });
470     }
471
472     static waitForKeyboardToHide()
473     {
474         if (!this.isWebKit2() || !this.isIOSFamily())
475             return Promise.resolve();
476
477         return new Promise(resolve => {
478             testRunner.runUIScript(`
479                 (function() {
480                     if (uiController.isShowingKeyboard)
481                         uiController.didHideKeyboardCallback = () => uiController.uiScriptComplete();
482                     else
483                         uiController.uiScriptComplete();
484                 })()`, resolve);
485         });
486     }
487
488     static getUICaretRect()
489     {
490         if (!this.isWebKit2() || !this.isIOSFamily())
491             return Promise.resolve();
492
493         return new Promise(resolve => {
494             testRunner.runUIScript(`(function() {
495                 uiController.doAfterNextStablePresentationUpdate(function() {
496                     uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
497                 });
498             })()`, jsonString => {
499                 resolve(JSON.parse(jsonString));
500             });
501         });
502     }
503
504     static getUISelectionRects()
505     {
506         if (!this.isWebKit2() || !this.isIOSFamily())
507             return Promise.resolve();
508
509         return new Promise(resolve => {
510             testRunner.runUIScript(`(function() {
511                 uiController.doAfterNextStablePresentationUpdate(function() {
512                     uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionRangeRects));
513                 });
514             })()`, jsonString => {
515                 resolve(JSON.parse(jsonString));
516             });
517         });
518     }
519
520     static getUICaretViewRect()
521     {
522         if (!this.isWebKit2() || !this.isIOSFamily())
523             return Promise.resolve();
524
525         return new Promise(resolve => {
526             testRunner.runUIScript(`(function() {
527                 uiController.doAfterNextStablePresentationUpdate(function() {
528                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionCaretViewRect));
529                 });
530             })()`, jsonString => {
531                 resolve(JSON.parse(jsonString));
532             });
533         });
534     }
535
536     static getUISelectionViewRects()
537     {
538         if (!this.isWebKit2() || !this.isIOSFamily())
539             return Promise.resolve();
540
541         return new Promise(resolve => {
542             testRunner.runUIScript(`(function() {
543                 uiController.doAfterNextStablePresentationUpdate(function() {
544                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
545                 });
546             })()`, jsonString => {
547                 resolve(JSON.parse(jsonString));
548             });
549         });
550     }
551
552     static getSelectionStartGrabberViewRect()
553     {
554         if (!this.isWebKit2() || !this.isIOSFamily())
555             return Promise.resolve();
556
557         return new Promise(resolve => {
558             testRunner.runUIScript(`(function() {
559                 uiController.doAfterNextStablePresentationUpdate(function() {
560                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionStartGrabberViewRect));
561                 });
562             })()`, jsonString => {
563                 resolve(JSON.parse(jsonString));
564             });
565         });
566     }
567
568     static getSelectionEndGrabberViewRect()
569     {
570         if (!this.isWebKit2() || !this.isIOSFamily())
571             return Promise.resolve();
572
573         return new Promise(resolve => {
574             testRunner.runUIScript(`(function() {
575                 uiController.doAfterNextStablePresentationUpdate(function() {
576                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionEndGrabberViewRect));
577                 });
578             })()`, jsonString => {
579                 resolve(JSON.parse(jsonString));
580             });
581         });
582     }
583
584     static replaceTextAtRange(text, location, length) {
585         return new Promise(resolve => {
586             testRunner.runUIScript(`(() => {
587                 uiController.replaceTextAtRange("${text}", ${location}, ${length});
588                 uiController.uiScriptComplete();
589             })()`, resolve);
590         });
591     }
592
593     static wait(promise)
594     {
595         testRunner.waitUntilDone();
596         if (window.finishJSTest)
597             window.jsTestIsAsync = true;
598
599         let finish = () => {
600             if (window.finishJSTest)
601                 finishJSTest();
602             else
603                 testRunner.notifyDone();
604         }
605
606         return promise.then(finish, finish);
607     }
608
609     static withUserGesture(callback)
610     {
611         internals.withUserGesture(callback);
612     }
613
614     static selectFormAccessoryPickerRow(rowIndex)
615     {
616         const selectRowScript = `(() => uiController.selectFormAccessoryPickerRow(${rowIndex}))()`;
617         return new Promise(resolve => testRunner.runUIScript(selectRowScript, resolve));
618     }
619
620     static selectFormPopoverTitle()
621     {
622         return new Promise(resolve => {
623             testRunner.runUIScript(`(() => {
624                 uiController.uiScriptComplete(uiController.selectFormPopoverTitle);
625             })()`, resolve);
626         });
627     }
628
629     static enterText(text)
630     {
631         const escapedText = text.replace(/`/g, "\\`");
632         const enterTextScript = `(() => uiController.enterText(\`${escapedText}\`))()`;
633         return new Promise(resolve => testRunner.runUIScript(enterTextScript, resolve));
634     }
635
636     static setTimePickerValue(hours, minutes)
637     {
638         const setValueScript = `(() => uiController.setTimePickerValue(${hours}, ${minutes}))()`;
639         return new Promise(resolve => testRunner.runUIScript(setValueScript, resolve));
640     }
641
642     static setShareSheetCompletesImmediatelyWithResolution(resolved)
643     {
644         const resolveShareSheet = `(() => uiController.setShareSheetCompletesImmediatelyWithResolution(${resolved}))()`;
645         return new Promise(resolve => testRunner.runUIScript(resolveShareSheet, resolve));
646     }
647
648     static textContentType()
649     {
650         return new Promise(resolve => {
651             testRunner.runUIScript(`(() => {
652                 uiController.uiScriptComplete(uiController.textContentType);
653             })()`, resolve);
654         });
655     }
656
657     static formInputLabel()
658     {
659         return new Promise(resolve => {
660             testRunner.runUIScript(`(() => {
661                 uiController.uiScriptComplete(uiController.formInputLabel);
662             })()`, resolve);
663         });
664     }
665
666     static isShowingDataListSuggestions()
667     {
668         return new Promise(resolve => {
669             testRunner.runUIScript(`(() => {
670                 uiController.uiScriptComplete(uiController.isShowingDataListSuggestions);
671             })()`, result => resolve(result === "true"));
672         });
673     }
674
675     static zoomScale()
676     {
677         return new Promise(resolve => {
678             testRunner.runUIScript(`(() => {
679                 uiController.uiScriptComplete(uiController.zoomScale);
680             })()`, scaleAsString => resolve(parseFloat(scaleAsString)));
681         });
682     }
683
684     static zoomToScale(scale)
685     {
686         const uiScript = `uiController.zoomToScale(${scale}, () => uiController.uiScriptComplete(uiController.zoomScale))`;
687         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
688     }
689
690     static typeCharacter(characterString)
691     {
692         if (!this.isWebKit2() || !this.isIOSFamily()) {
693             eventSender.keyDown(characterString);
694             return;
695         }
696
697         const escapedString = characterString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
698         const uiScript = `uiController.typeCharacterUsingHardwareKeyboard(\`${escapedString}\`, () => uiController.uiScriptComplete())`;
699         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
700     }
701
702     static applyAutocorrection(newText, oldText)
703     {
704         if (!this.isWebKit2())
705             return;
706
707         const [escapedNewText, escapedOldText] = [newText.replace(/`/g, "\\`"), oldText.replace(/`/g, "\\`")];
708         const uiScript = `uiController.applyAutocorrection(\`${escapedNewText}\`, \`${escapedOldText}\`, () => uiController.uiScriptComplete())`;
709         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
710     }
711
712     static inputViewBounds()
713     {
714         if (!this.isWebKit2() || !this.isIOSFamily())
715             return Promise.resolve();
716
717         return new Promise(resolve => {
718             testRunner.runUIScript(`(() => {
719                 uiController.uiScriptComplete(JSON.stringify(uiController.inputViewBounds));
720             })()`, jsonString => {
721                 resolve(JSON.parse(jsonString));
722             });
723         });
724     }
725
726     static calendarType()
727     {
728         if (!this.isWebKit2())
729             return Promise.resolve();
730
731         return new Promise(resolve => {
732             testRunner.runUIScript(`(() => {
733                 uiController.doAfterNextStablePresentationUpdate(() => {
734                     uiController.uiScriptComplete(JSON.stringify(uiController.calendarType));
735                 })
736             })()`, jsonString => {
737                 resolve(JSON.parse(jsonString));
738             });
739         });
740     }
741
742     static setDefaultCalendarType(calendarIdentifier)
743     {
744         if (!this.isWebKit2())
745             return Promise.resolve();
746
747         return new Promise(resolve => testRunner.runUIScript(`uiController.setDefaultCalendarType('${calendarIdentifier}')`, resolve));
748
749     }
750
751     static setViewScale(scale)
752     {
753         if (!this.isWebKit2())
754             return Promise.resolve();
755
756         return new Promise(resolve => testRunner.runUIScript(`uiController.setViewScale(${scale})`, resolve));
757     }
758
759     static resignFirstResponder()
760     {
761         if (!this.isWebKit2())
762             return Promise.resolve();
763
764         return new Promise(resolve => testRunner.runUIScript(`uiController.resignFirstResponder()`, resolve));
765     }
766
767     static minimumZoomScale()
768     {
769         if (!this.isWebKit2())
770             return Promise.resolve();
771
772         return new Promise(resolve => {
773             testRunner.runUIScript(`(() => {
774                 uiController.uiScriptComplete(uiController.minimumZoomScale);
775             })()`, scaleAsString => resolve(parseFloat(scaleAsString)))
776         });
777     }
778
779     static drawSquareInEditableImage()
780     {
781         if (!this.isWebKit2())
782             return Promise.resolve();
783
784         return new Promise(resolve => testRunner.runUIScript(`uiController.drawSquareInEditableImage()`, resolve));
785     }
786
787     static stylusTapAt(x, y, modifiers=[])
788     {
789         if (!this.isWebKit2())
790             return Promise.resolve();
791
792         return new Promise((resolve) => {
793             testRunner.runUIScript(`
794                 uiController.stylusTapAtPointWithModifiers(${x}, ${y}, 2, 1, 0.5, ${JSON.stringify(modifiers)}, function() {
795                     uiController.uiScriptComplete();
796                 });`, resolve);
797         });
798     }
799
800     static numberOfStrokesInEditableImage()
801     {
802         if (!this.isWebKit2())
803             return Promise.resolve();
804
805         return new Promise(resolve => {
806             testRunner.runUIScript(`(() => {
807                 uiController.uiScriptComplete(uiController.numberOfStrokesInEditableImage);
808             })()`, numberAsString => resolve(parseInt(numberAsString, 10)))
809         });
810     }
811
812     static attachmentInfo(attachmentIdentifier)
813     {
814         if (!this.isWebKit2())
815             return Promise.resolve();
816
817         return new Promise(resolve => {
818             testRunner.runUIScript(`(() => {
819                 uiController.uiScriptComplete(JSON.stringify(uiController.attachmentInfo('${attachmentIdentifier}')));
820             })()`, jsonString => {
821                 resolve(JSON.parse(jsonString));
822             })
823         });
824     }
825
826     static setMinimumEffectiveWidth(effectiveWidth)
827     {
828         if (!this.isWebKit2())
829             return Promise.resolve();
830
831         return new Promise(resolve => testRunner.runUIScript(`uiController.setMinimumEffectiveWidth(${effectiveWidth})`, resolve));
832     }
833
834     static setAllowsViewportShrinkToFit(allows)
835     {
836         if (!this.isWebKit2())
837             return Promise.resolve();
838
839         return new Promise(resolve => testRunner.runUIScript(`uiController.setAllowsViewportShrinkToFit(${allows})`, resolve));
840     }
841
842     static setKeyboardInputModeIdentifier(identifier)
843     {
844         if (!this.isWebKit2())
845             return Promise.resolve();
846
847         const escapedIdentifier = identifier.replace(/`/g, "\\`");
848         return new Promise(resolve => testRunner.runUIScript(`uiController.setKeyboardInputModeIdentifier(\`${escapedIdentifier}\`)`, resolve));
849     }
850
851     static contentOffset()
852     {
853         if (!this.isIOSFamily())
854             return Promise.resolve();
855
856         const uiScript = "JSON.stringify([uiController.contentOffsetX, uiController.contentOffsetY])";
857         return new Promise(resolve => testRunner.runUIScript(uiScript, result => {
858             const [offsetX, offsetY] = JSON.parse(result)
859             resolve({ x: offsetX, y: offsetY });
860         }));
861     }
862
863     static undoAndRedoLabels()
864     {
865         if (!this.isWebKit2())
866             return Promise.resolve();
867
868         const script = "JSON.stringify([uiController.lastUndoLabel, uiController.firstRedoLabel])";
869         return new Promise(resolve => testRunner.runUIScript(script, result => resolve(JSON.parse(result))));
870     }
871
872     static waitForMenuToShow()
873     {
874         return new Promise(resolve => {
875             testRunner.runUIScript(`
876                 (function() {
877                     if (!uiController.isShowingMenu)
878                         uiController.didShowMenuCallback = () => uiController.uiScriptComplete();
879                     else
880                         uiController.uiScriptComplete();
881                 })()`, resolve);
882         });
883     }
884
885     static waitForMenuToHide()
886     {
887         return new Promise(resolve => {
888             testRunner.runUIScript(`
889                 (function() {
890                     if (uiController.isShowingMenu)
891                         uiController.didHideMenuCallback = () => uiController.uiScriptComplete();
892                     else
893                         uiController.uiScriptComplete();
894                 })()`, resolve);
895         });
896     }
897
898     static isShowingMenu()
899     {
900         return new Promise(resolve => {
901             testRunner.runUIScript(`uiController.isShowingMenu`, result => resolve(result === "true"));
902         });
903     }
904
905     static isDismissingMenu()
906     {
907         return new Promise(resolve => {
908             testRunner.runUIScript(`uiController.isDismissingMenu`, result => resolve(result === "true"));
909         });
910     }
911
912     static menuRect()
913     {
914         return new Promise(resolve => {
915             testRunner.runUIScript("JSON.stringify(uiController.menuRect)", result => resolve(JSON.parse(result)));
916         });
917     }
918
919     static setHardwareKeyboardAttached(attached)
920     {
921         return new Promise(resolve => testRunner.runUIScript(`uiController.setHardwareKeyboardAttached(${attached ? "true" : "false"})`, resolve));
922     }
923
924     static rectForMenuAction(action)
925     {
926         return new Promise(resolve => {
927             testRunner.runUIScript(`
928                 const rect = uiController.rectForMenuAction("${action}");
929                 uiController.uiScriptComplete(rect ? JSON.stringify(rect) : "");
930             `, stringResult => {
931                 resolve(stringResult.length ? JSON.parse(stringResult) : null);
932             });
933         });
934     }
935
936     static async chooseMenuAction(action)
937     {
938         const menuRect = await this.rectForMenuAction(action);
939         if (menuRect)
940             await this.activateAt(menuRect.left + menuRect.width / 2, menuRect.top + menuRect.height / 2);
941     }
942
943     static callFunctionAndWaitForEvent(functionToCall, target, eventName)
944     {
945         return new Promise((resolve) => {
946             target.addEventListener(eventName, resolve, { once: true });
947             functionToCall();
948         });
949
950     }
951
952     static callFunctionAndWaitForScrollToFinish(functionToCall, ...theArguments)
953     {
954         return new Promise((resolved) => {
955             function scrollDidFinish() {
956                 window.removeEventListener("scroll", handleScroll, true);
957                 resolved();
958             }
959
960             let lastScrollTimerId = 0; // When the timer with this id fires then the page has finished scrolling.
961             function handleScroll() {
962                 if (lastScrollTimerId) {
963                     window.clearTimeout(lastScrollTimerId);
964                     lastScrollTimerId = 0;
965                 }
966                 lastScrollTimerId = window.setTimeout(scrollDidFinish, 300); // Over 250ms to give some room for error.
967             }
968             window.addEventListener("scroll", handleScroll, true);
969
970             functionToCall.apply(this, theArguments);
971         });
972     }
973
974     static rotateDevice(orientationName, animatedResize = false)
975     {
976         if (!this.isWebKit2() || !this.isIOSFamily())
977             return Promise.resolve();
978
979         return new Promise(resolve => {
980             testRunner.runUIScript(`(() => {
981                 uiController.${animatedResize ? "simulateRotationLikeSafari" : "simulateRotation"}("${orientationName}", function() {
982                     uiController.doAfterVisibleContentRectUpdate(() => uiController.uiScriptComplete());
983                 });
984             })()`, resolve);
985         });
986     }
987
988     static getScrollingTree()
989     {
990         if (!this.isWebKit2() || !this.isIOSFamily())
991             return Promise.resolve();
992
993         return new Promise(resolve => {
994             testRunner.runUIScript(`(() => {
995                 return uiController.scrollingTreeAsText;
996             })()`, resolve);
997         });
998     }
999 }