79d0c76c6c69008f4271009ecd5f5b0e545c40e1
[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 async moveMouseAndWaitForFrame(x, y)
33     {
34         eventSender.mouseMoveTo(x, y);
35         await UIHelper.animationFrame();
36     }
37
38     static async mouseWheelScrollAt(x, y, beginX, beginY, deltaX, deltaY)
39     {
40         if (beginX === undefined)
41             beginX = 0;
42         if (beginY === undefined)
43             beginY = -1;
44
45         if (deltaX === undefined)
46             deltaX = 0;
47         if (deltaY === undefined)
48             deltaY = -10;
49
50         eventSender.monitorWheelEvents();
51         eventSender.mouseMoveTo(x, y);
52         eventSender.mouseScrollByWithWheelAndMomentumPhases(beginX, beginY, "began", "none");
53         eventSender.mouseScrollByWithWheelAndMomentumPhases(deltaX, deltaY, "changed", "none");
54         eventSender.mouseScrollByWithWheelAndMomentumPhases(0, 0, "ended", "none");
55         return new Promise(resolve => {
56             eventSender.callAfterScrollingCompletes(() => {
57                 requestAnimationFrame(resolve);
58             });
59         });
60     }
61
62     static async mouseWheelMayBeginAt(x, y)
63     {
64         eventSender.mouseMoveTo(x, y);
65         eventSender.mouseScrollByWithWheelAndMomentumPhases(x, y, "maybegin", "none");
66         await UIHelper.animationFrame();
67     }
68
69     static async mouseWheelCancelAt(x, y)
70     {
71         eventSender.mouseMoveTo(x, y);
72         eventSender.mouseScrollByWithWheelAndMomentumPhases(x, y, "cancelled", "none");
73         await UIHelper.animationFrame();
74     }
75
76     static async waitForScrollCompletion()
77     {
78         return new Promise(resolve => {
79             eventSender.callAfterScrollingCompletes(() => {
80                 requestAnimationFrame(resolve);
81             });
82         });
83     }
84
85     static async animationFrame()
86     {
87         return new Promise(requestAnimationFrame);
88     }
89
90     static async waitForCondition(conditionFunc)
91     {
92         while (!conditionFunc()) {
93             await UIHelper.animationFrame();
94         }
95     }
96
97     static sendEventStream(eventStream)
98     {
99         const eventStreamAsString = JSON.stringify(eventStream);
100         return new Promise(resolve => {
101             testRunner.runUIScript(`
102                 (function() {
103                     uiController.sendEventStream(\`${eventStreamAsString}\`, () => {
104                         uiController.uiScriptComplete();
105                     });
106                 })();
107             `, resolve);
108         });
109     }
110
111     static tapAt(x, y, modifiers=[])
112     {
113         console.assert(this.isIOSFamily());
114
115         if (!this.isWebKit2()) {
116             console.assert(!modifiers || !modifiers.length);
117             eventSender.addTouchPoint(x, y);
118             eventSender.touchStart();
119             eventSender.releaseTouchPoint(0);
120             eventSender.touchEnd();
121             return Promise.resolve();
122         }
123
124         return new Promise((resolve) => {
125             testRunner.runUIScript(`
126                 uiController.singleTapAtPointWithModifiers(${x}, ${y}, ${JSON.stringify(modifiers)}, function() {
127                     uiController.uiScriptComplete();
128                 });`, resolve);
129         });
130     }
131
132     static doubleTapAt(x, y, delay = 0)
133     {
134         console.assert(this.isIOSFamily());
135
136         if (!this.isWebKit2()) {
137             eventSender.addTouchPoint(x, y);
138             eventSender.touchStart();
139             eventSender.releaseTouchPoint(0);
140             eventSender.touchEnd();
141             eventSender.addTouchPoint(x, y);
142             eventSender.touchStart();
143             eventSender.releaseTouchPoint(0);
144             eventSender.touchEnd();
145             return Promise.resolve();
146         }
147
148         return new Promise((resolve) => {
149             testRunner.runUIScript(`
150                 uiController.doubleTapAtPoint(${x}, ${y}, ${delay}, function() {
151                     uiController.uiScriptComplete();
152                 });`, resolve);
153         });
154     }
155
156     static humanSpeedDoubleTapAt(x, y)
157     {
158         console.assert(this.isIOSFamily());
159
160         if (!this.isWebKit2()) {
161             // FIXME: Add a sleep in here.
162             eventSender.addTouchPoint(x, y);
163             eventSender.touchStart();
164             eventSender.releaseTouchPoint(0);
165             eventSender.touchEnd();
166             eventSender.addTouchPoint(x, y);
167             eventSender.touchStart();
168             eventSender.releaseTouchPoint(0);
169             eventSender.touchEnd();
170             return Promise.resolve();
171         }
172
173         return UIHelper.doubleTapAt(x, y, 0.12);
174     }
175
176     static humanSpeedZoomByDoubleTappingAt(x, y)
177     {
178         console.assert(this.isIOSFamily());
179
180         if (!this.isWebKit2()) {
181             // FIXME: Add a sleep in here.
182             eventSender.addTouchPoint(x, y);
183             eventSender.touchStart();
184             eventSender.releaseTouchPoint(0);
185             eventSender.touchEnd();
186             eventSender.addTouchPoint(x, y);
187             eventSender.touchStart();
188             eventSender.releaseTouchPoint(0);
189             eventSender.touchEnd();
190             return Promise.resolve();
191         }
192
193         return new Promise(async (resolve) => {
194             testRunner.runUIScript(`
195                 uiController.didEndZoomingCallback = () => {
196                     uiController.didEndZoomingCallback = null;
197                     uiController.uiScriptComplete(uiController.zoomScale);
198                 };
199                 uiController.doubleTapAtPoint(${x}, ${y}, 0.12, () => { });`, resolve);
200         });
201     }
202
203     static zoomByDoubleTappingAt(x, y)
204     {
205         console.assert(this.isIOSFamily());
206
207         if (!this.isWebKit2()) {
208             eventSender.addTouchPoint(x, y);
209             eventSender.touchStart();
210             eventSender.releaseTouchPoint(0);
211             eventSender.touchEnd();
212             eventSender.addTouchPoint(x, y);
213             eventSender.touchStart();
214             eventSender.releaseTouchPoint(0);
215             eventSender.touchEnd();
216             return Promise.resolve();
217         }
218
219         return new Promise((resolve) => {
220             testRunner.runUIScript(`
221                 uiController.didEndZoomingCallback = () => {
222                     uiController.didEndZoomingCallback = null;
223                     uiController.uiScriptComplete(uiController.zoomScale);
224                 };
225                 uiController.doubleTapAtPoint(${x}, ${y}, 0, () => { });`, resolve);
226         });
227     }
228
229     static activateAt(x, y)
230     {
231         if (!this.isWebKit2() || !this.isIOSFamily()) {
232             eventSender.mouseMoveTo(x, y);
233             eventSender.mouseDown();
234             eventSender.mouseUp();
235             return Promise.resolve();
236         }
237
238         return new Promise((resolve) => {
239             testRunner.runUIScript(`
240                 uiController.singleTapAtPoint(${x}, ${y}, function() {
241                     uiController.uiScriptComplete();
242                 });`, resolve);
243         });
244     }
245
246     static activateElement(element)
247     {
248         const x = element.offsetLeft + element.offsetWidth / 2;
249         const y = element.offsetTop + element.offsetHeight / 2;
250         return UIHelper.activateAt(x, y);
251     }
252
253     static async doubleActivateAt(x, y)
254     {
255         if (this.isIOSFamily())
256             await UIHelper.doubleTapAt(x, y);
257         else
258             await UIHelper.doubleClickAt(x, y);
259     }
260
261     static async doubleActivateAtSelectionStart()
262     {
263         const rects = window.getSelection().getRangeAt(0).getClientRects();
264         const x = rects[0].left;
265         const y = rects[0].top;
266         if (this.isIOSFamily()) {
267             await UIHelper.activateAndWaitForInputSessionAt(x, y);
268             await UIHelper.doubleTapAt(x, y);
269             // This is only here to deal with async/sync copy/paste calls, so
270             // once <rdar://problem/16207002> is resolved, should be able to remove for faster tests.
271             await new Promise(resolve => testRunner.runUIScript("uiController.uiScriptComplete()", resolve));
272         } else
273             await UIHelper.doubleClickAt(x, y);
274     }
275
276     static async selectWordByDoubleTapOrClick(element, relativeX = 5, relativeY = 5)
277     {
278         const boundingRect = element.getBoundingClientRect();
279         const x = boundingRect.x + relativeX;
280         const y = boundingRect.y + relativeY;
281         if (this.isIOSFamily()) {
282             await UIHelper.activateAndWaitForInputSessionAt(x, y);
283             await UIHelper.doubleTapAt(x, y);
284             // This is only here to deal with async/sync copy/paste calls, so
285             // once <rdar://problem/16207002> is resolved, should be able to remove for faster tests.
286             await new Promise(resolve => testRunner.runUIScript("uiController.uiScriptComplete()", resolve));
287         } else {
288             await UIHelper.doubleClickAt(x, y);
289         }
290     }
291
292     static keyDown(key, modifiers=[])
293     {
294         if (!this.isWebKit2() || !this.isIOSFamily()) {
295             eventSender.keyDown(key, modifiers);
296             return Promise.resolve();
297         }
298
299         return new Promise((resolve) => {
300             testRunner.runUIScript(`uiController.keyDown("${key}", ${JSON.stringify(modifiers)});`, resolve);
301         });
302     }
303
304     static toggleCapsLock()
305     {
306         return new Promise((resolve) => {
307             testRunner.runUIScript(`uiController.toggleCapsLock(() => uiController.uiScriptComplete());`, resolve);
308         });
309     }
310
311     static keyboardIsAutomaticallyShifted()
312     {
313         return new Promise(resolve => {
314             testRunner.runUIScript(`uiController.keyboardIsAutomaticallyShifted`, result => resolve(result === "true"));
315         });
316     }
317
318     static ensurePresentationUpdate()
319     {
320         if (!this.isWebKit2()) {
321             testRunner.display();
322             return Promise.resolve();
323         }
324
325         return new Promise(resolve => {
326             testRunner.runUIScript(`
327                 uiController.doAfterPresentationUpdate(function() {
328                     uiController.uiScriptComplete();
329                 });`, resolve);
330         });
331     }
332
333     static ensureStablePresentationUpdate()
334     {
335         if (!this.isWebKit2()) {
336             testRunner.display();
337             return Promise.resolve();
338         }
339
340         return new Promise(resolve => {
341             testRunner.runUIScript(`
342                 uiController.doAfterNextStablePresentationUpdate(function() {
343                     uiController.uiScriptComplete();
344                 });`, resolve);
345         });
346     }
347
348     static ensurePositionInformationUpdateForElement(element)
349     {
350         const boundingRect = element.getBoundingClientRect();
351         const x = boundingRect.x + 5;
352         const y = boundingRect.y + 5;
353
354         if (!this.isWebKit2()) {
355             testRunner.display();
356             return Promise.resolve();
357         }
358
359         return new Promise(resolve => {
360             testRunner.runUIScript(`
361                 uiController.ensurePositionInformationIsUpToDateAt(${x}, ${y}, function () {
362                     uiController.uiScriptComplete();
363                 });`, resolve);
364         });
365     }
366
367     static delayFor(ms)
368     {
369         return new Promise(resolve => setTimeout(resolve, ms));
370     }
371     
372     static immediateScrollTo(x, y)
373     {
374         if (!this.isWebKit2()) {
375             window.scrollTo(x, y);
376             return Promise.resolve();
377         }
378
379         return new Promise(resolve => {
380             testRunner.runUIScript(`
381                 uiController.immediateScrollToOffset(${x}, ${y});`, resolve);
382         });
383     }
384
385     static immediateUnstableScrollTo(x, y)
386     {
387         if (!this.isWebKit2()) {
388             window.scrollTo(x, y);
389             return Promise.resolve();
390         }
391
392         return new Promise(resolve => {
393             testRunner.runUIScript(`
394                 uiController.stableStateOverride = false;
395                 uiController.immediateScrollToOffset(${x}, ${y});`, resolve);
396         });
397     }
398
399     static immediateScrollElementAtContentPointToOffset(x, y, scrollX, scrollY, scrollUpdatesDisabled = false)
400     {
401         if (!this.isWebKit2())
402             return Promise.resolve();
403
404         return new Promise(resolve => {
405             testRunner.runUIScript(`
406                 uiController.scrollUpdatesDisabled = ${scrollUpdatesDisabled};
407                 uiController.immediateScrollElementAtContentPointToOffset(${x}, ${y}, ${scrollX}, ${scrollY});`, resolve);
408         });
409     }
410
411     static ensureVisibleContentRectUpdate()
412     {
413         if (!this.isWebKit2())
414             return Promise.resolve();
415
416         return new Promise(resolve => {
417             const visibleContentRectUpdateScript = "uiController.doAfterVisibleContentRectUpdate(() => uiController.uiScriptComplete())";
418             testRunner.runUIScript(visibleContentRectUpdateScript, resolve);
419         });
420     }
421
422     static longPressAndGetContextMenuContentAt(x, y)
423     {
424         return new Promise(resolve => {
425             testRunner.runUIScript(`
426             (function() {
427                 uiController.didShowContextMenuCallback = function() {
428                     uiController.uiScriptComplete(JSON.stringify(uiController.contentsOfUserInterfaceItem('contextMenu')));
429                 };
430                 uiController.longPressAtPoint(${x}, ${y}, function() { });
431             })();`, result => resolve(JSON.parse(result)));
432         });
433     }
434
435     static activateAndWaitForInputSessionAt(x, y)
436     {
437         if (!this.isWebKit2() || !this.isIOSFamily())
438             return this.activateAt(x, y);
439
440         return new Promise(resolve => {
441             testRunner.runUIScript(`
442                 (function() {
443                     function clearCallbacksAndScriptComplete() {
444                         uiController.didShowKeyboardCallback = null;
445                         uiController.willPresentPopoverCallback = null;
446                         uiController.uiScriptComplete();
447                     }
448                     uiController.didShowKeyboardCallback = clearCallbacksAndScriptComplete;
449                     uiController.willPresentPopoverCallback = clearCallbacksAndScriptComplete;
450                     uiController.singleTapAtPoint(${x}, ${y}, function() { });
451                 })()`, resolve);
452         });
453     }
454
455     static waitForInputSessionToDismiss()
456     {
457         return new Promise(resolve => {
458             testRunner.runUIScript(`
459                 (function() {
460                     function clearCallbacksAndScriptComplete() {
461                         uiController.didHideKeyboardCallback = null;
462                         uiController.didDismissPopoverCallback = null;
463                         uiController.uiScriptComplete();
464                     }
465                     uiController.didHideKeyboardCallback = clearCallbacksAndScriptComplete;
466                     uiController.didDismissPopoverCallback = clearCallbacksAndScriptComplete;
467                 })()`, resolve);
468         });
469     }
470
471     static activateElementAndWaitForInputSession(element)
472     {
473         const x = element.offsetLeft + element.offsetWidth / 2;
474         const y = element.offsetTop + element.offsetHeight / 2;
475         return this.activateAndWaitForInputSessionAt(x, y);
476     }
477
478     static activateFormControl(element)
479     {
480         if (!this.isWebKit2() || !this.isIOSFamily())
481             return this.activateElement(element);
482
483         const x = element.offsetLeft + element.offsetWidth / 2;
484         const y = element.offsetTop + element.offsetHeight / 2;
485
486         return new Promise(resolve => {
487             testRunner.runUIScript(`
488                 (function() {
489                     uiController.didStartFormControlInteractionCallback = function() {
490                         uiController.uiScriptComplete();
491                     };
492                     uiController.singleTapAtPoint(${x}, ${y}, function() { });
493                 })()`, resolve);
494         });
495     }
496
497     static dismissFormAccessoryView()
498     {
499         if (!this.isWebKit2() || !this.isIOSFamily())
500             return Promise.resolve();
501
502         return new Promise(resolve => {
503             testRunner.runUIScript(`
504                 (function() {
505                     uiController.dismissFormAccessoryView();
506                     uiController.uiScriptComplete();
507                 })()`, resolve);
508         });
509     }
510
511     static isShowingKeyboard()
512     {
513         return new Promise(resolve => {
514             testRunner.runUIScript("uiController.isShowingKeyboard", result => resolve(result === "true"));
515         });
516     }
517
518     static hasInputSession()
519     {
520         return new Promise(resolve => {
521             testRunner.runUIScript("uiController.hasInputSession", result => resolve(result === "true"));
522         });
523     }
524
525     static isPresentingModally()
526     {
527         return new Promise(resolve => {
528             testRunner.runUIScript("uiController.isPresentingModally", result => resolve(result === "true"));
529         });
530     }
531
532     static deactivateFormControl(element)
533     {
534         if (!this.isWebKit2() || !this.isIOSFamily()) {
535             element.blur();
536             return Promise.resolve();
537         }
538
539         return new Promise(async resolve => {
540             element.blur();
541             while (await this.isPresentingModally())
542                 continue;
543             while (await this.isShowingKeyboard())
544                 continue;
545             resolve();
546         });
547     }
548
549     static waitForPopoverToPresent()
550     {
551         if (!this.isWebKit2() || !this.isIOSFamily())
552             return Promise.resolve();
553
554         return new Promise(resolve => {
555             testRunner.runUIScript(`
556                 (function() {
557                     if (uiController.isShowingPopover)
558                         uiController.uiScriptComplete();
559                     else
560                         uiController.willPresentPopoverCallback = () => uiController.uiScriptComplete();
561                 })()`, resolve);
562         });
563     }
564
565     static waitForPopoverToDismiss()
566     {
567         if (!this.isWebKit2() || !this.isIOSFamily())
568             return Promise.resolve();
569
570         return new Promise(resolve => {
571             testRunner.runUIScript(`
572                 (function() {
573                     if (uiController.isShowingPopover)
574                         uiController.didDismissPopoverCallback = () => uiController.uiScriptComplete();
575                     else
576                         uiController.uiScriptComplete();
577                 })()`, resolve);
578         });
579     }
580
581     static waitForKeyboardToHide()
582     {
583         if (!this.isWebKit2() || !this.isIOSFamily())
584             return Promise.resolve();
585
586         return new Promise(resolve => {
587             testRunner.runUIScript(`
588                 (function() {
589                     if (uiController.isShowingKeyboard)
590                         uiController.didHideKeyboardCallback = () => uiController.uiScriptComplete();
591                     else
592                         uiController.uiScriptComplete();
593                 })()`, resolve);
594         });
595     }
596
597     static getUICaretRect()
598     {
599         if (!this.isWebKit2() || !this.isIOSFamily())
600             return Promise.resolve();
601
602         return new Promise(resolve => {
603             testRunner.runUIScript(`(function() {
604                 uiController.doAfterNextStablePresentationUpdate(function() {
605                     uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionCaretRect));
606                 });
607             })()`, jsonString => {
608                 resolve(JSON.parse(jsonString));
609             });
610         });
611     }
612
613     static getUISelectionRects()
614     {
615         if (!this.isWebKit2() || !this.isIOSFamily())
616             return Promise.resolve();
617
618         return new Promise(resolve => {
619             testRunner.runUIScript(`(function() {
620                 uiController.doAfterNextStablePresentationUpdate(function() {
621                     uiController.uiScriptComplete(JSON.stringify(uiController.textSelectionRangeRects));
622                 });
623             })()`, jsonString => {
624                 resolve(JSON.parse(jsonString));
625             });
626         });
627     }
628
629     static getUICaretViewRect()
630     {
631         if (!this.isWebKit2() || !this.isIOSFamily())
632             return Promise.resolve();
633
634         return new Promise(resolve => {
635             testRunner.runUIScript(`(function() {
636                 uiController.doAfterNextStablePresentationUpdate(function() {
637                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionCaretViewRect));
638                 });
639             })()`, jsonString => {
640                 resolve(JSON.parse(jsonString));
641             });
642         });
643     }
644
645     static getUISelectionViewRects()
646     {
647         if (!this.isWebKit2() || !this.isIOSFamily())
648             return Promise.resolve();
649
650         return new Promise(resolve => {
651             testRunner.runUIScript(`(function() {
652                 uiController.doAfterNextStablePresentationUpdate(function() {
653                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionRangeViewRects));
654                 });
655             })()`, jsonString => {
656                 resolve(JSON.parse(jsonString));
657             });
658         });
659     }
660
661     static getSelectionStartGrabberViewRect()
662     {
663         if (!this.isWebKit2() || !this.isIOSFamily())
664             return Promise.resolve();
665
666         return new Promise(resolve => {
667             testRunner.runUIScript(`(function() {
668                 uiController.doAfterNextStablePresentationUpdate(function() {
669                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionStartGrabberViewRect));
670                 });
671             })()`, jsonString => {
672                 resolve(JSON.parse(jsonString));
673             });
674         });
675     }
676
677     static getSelectionEndGrabberViewRect()
678     {
679         if (!this.isWebKit2() || !this.isIOSFamily())
680             return Promise.resolve();
681
682         return new Promise(resolve => {
683             testRunner.runUIScript(`(function() {
684                 uiController.doAfterNextStablePresentationUpdate(function() {
685                     uiController.uiScriptComplete(JSON.stringify(uiController.selectionEndGrabberViewRect));
686                 });
687             })()`, jsonString => {
688                 resolve(JSON.parse(jsonString));
689             });
690         });
691     }
692
693     static replaceTextAtRange(text, location, length) {
694         return new Promise(resolve => {
695             testRunner.runUIScript(`(() => {
696                 uiController.replaceTextAtRange("${text}", ${location}, ${length});
697                 uiController.uiScriptComplete();
698             })()`, resolve);
699         });
700     }
701
702     static wait(promise)
703     {
704         testRunner.waitUntilDone();
705         if (window.finishJSTest)
706             window.jsTestIsAsync = true;
707
708         let finish = () => {
709             if (window.finishJSTest)
710                 finishJSTest();
711             else
712                 testRunner.notifyDone();
713         }
714
715         return promise.then(finish, finish);
716     }
717
718     static withUserGesture(callback)
719     {
720         internals.withUserGesture(callback);
721     }
722
723     static selectFormAccessoryPickerRow(rowIndex)
724     {
725         const selectRowScript = `uiController.selectFormAccessoryPickerRow(${rowIndex})`;
726         return new Promise(resolve => testRunner.runUIScript(selectRowScript, resolve));
727     }
728
729     static selectFormAccessoryHasCheckedItemAtRow(rowIndex)
730     {
731         return new Promise(resolve => testRunner.runUIScript(`uiController.selectFormAccessoryHasCheckedItemAtRow(${rowIndex})`, result => {
732             resolve(result === "true");
733         }));
734     }
735
736     static selectFormPopoverTitle()
737     {
738         return new Promise(resolve => {
739             testRunner.runUIScript(`(() => {
740                 uiController.uiScriptComplete(uiController.selectFormPopoverTitle);
741             })()`, resolve);
742         });
743     }
744
745     static enterText(text)
746     {
747         const escapedText = text.replace(/`/g, "\\`");
748         const enterTextScript = `(() => uiController.enterText(\`${escapedText}\`))()`;
749         return new Promise(resolve => testRunner.runUIScript(enterTextScript, resolve));
750     }
751
752     static setTimePickerValue(hours, minutes)
753     {
754         const setValueScript = `(() => uiController.setTimePickerValue(${hours}, ${minutes}))()`;
755         return new Promise(resolve => testRunner.runUIScript(setValueScript, resolve));
756     }
757
758     static timerPickerValues()
759     {
760         if (!this.isIOSFamily())
761             return Promise.resolve();
762
763         const uiScript = "JSON.stringify([uiController.timePickerValueHour, uiController.timePickerValueMinute])";
764         return new Promise(resolve => testRunner.runUIScript(uiScript, result => {
765             const [hour, minute] = JSON.parse(result)
766             resolve({ hour: hour, minute: minute });
767         }));
768     }
769
770     static setShareSheetCompletesImmediatelyWithResolution(resolved)
771     {
772         const resolveShareSheet = `(() => uiController.setShareSheetCompletesImmediatelyWithResolution(${resolved}))()`;
773         return new Promise(resolve => testRunner.runUIScript(resolveShareSheet, resolve));
774     }
775
776     static textContentType()
777     {
778         return new Promise(resolve => {
779             testRunner.runUIScript(`(() => {
780                 uiController.uiScriptComplete(uiController.textContentType);
781             })()`, resolve);
782         });
783     }
784
785     static formInputLabel()
786     {
787         return new Promise(resolve => {
788             testRunner.runUIScript(`(() => {
789                 uiController.uiScriptComplete(uiController.formInputLabel);
790             })()`, resolve);
791         });
792     }
793
794     static activateDataListSuggestion(index) {
795         const script = `uiController.activateDataListSuggestion(${index}, () => {
796             uiController.uiScriptComplete("");
797         });`;
798         return new Promise(resolve => testRunner.runUIScript(script, resolve));
799     }
800
801     static isShowingDataListSuggestions()
802     {
803         return new Promise(resolve => {
804             testRunner.runUIScript(`(() => {
805                 uiController.uiScriptComplete(uiController.isShowingDataListSuggestions);
806             })()`, result => resolve(result === "true"));
807         });
808     }
809
810     static zoomScale()
811     {
812         return new Promise(resolve => {
813             testRunner.runUIScript(`(() => {
814                 uiController.uiScriptComplete(uiController.zoomScale);
815             })()`, scaleAsString => resolve(parseFloat(scaleAsString)));
816         });
817     }
818
819     static zoomToScale(scale)
820     {
821         const uiScript = `uiController.zoomToScale(${scale}, () => uiController.uiScriptComplete(uiController.zoomScale))`;
822         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
823     }
824
825     static immediateZoomToScale(scale)
826     {
827         const uiScript = `uiController.immediateZoomToScale(${scale})`;
828         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
829     }
830
831     static typeCharacter(characterString)
832     {
833         if (!this.isWebKit2() || !this.isIOSFamily()) {
834             eventSender.keyDown(characterString);
835             return;
836         }
837
838         const escapedString = characterString.replace(/\\/g, "\\\\").replace(/`/g, "\\`");
839         const uiScript = `uiController.typeCharacterUsingHardwareKeyboard(\`${escapedString}\`, () => uiController.uiScriptComplete())`;
840         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
841     }
842
843     static applyAutocorrection(newText, oldText)
844     {
845         if (!this.isWebKit2())
846             return;
847
848         const [escapedNewText, escapedOldText] = [newText.replace(/`/g, "\\`"), oldText.replace(/`/g, "\\`")];
849         const uiScript = `uiController.applyAutocorrection(\`${escapedNewText}\`, \`${escapedOldText}\`, () => uiController.uiScriptComplete())`;
850         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
851     }
852
853     static inputViewBounds()
854     {
855         if (!this.isWebKit2() || !this.isIOSFamily())
856             return Promise.resolve();
857
858         return new Promise(resolve => {
859             testRunner.runUIScript(`(() => {
860                 uiController.uiScriptComplete(JSON.stringify(uiController.inputViewBounds));
861             })()`, jsonString => {
862                 resolve(JSON.parse(jsonString));
863             });
864         });
865     }
866
867     static calendarType()
868     {
869         if (!this.isWebKit2())
870             return Promise.resolve();
871
872         return new Promise(resolve => {
873             testRunner.runUIScript(`(() => {
874                 uiController.doAfterNextStablePresentationUpdate(() => {
875                     uiController.uiScriptComplete(JSON.stringify(uiController.calendarType));
876                 })
877             })()`, jsonString => {
878                 resolve(JSON.parse(jsonString));
879             });
880         });
881     }
882
883     static setDefaultCalendarType(calendarIdentifier, localeIdentifier)
884     {
885         if (!this.isWebKit2())
886             return Promise.resolve();
887
888         return new Promise(resolve => testRunner.runUIScript(`uiController.setDefaultCalendarType('${calendarIdentifier}', '${localeIdentifier}')`, resolve));
889
890     }
891
892     static setViewScale(scale)
893     {
894         if (!this.isWebKit2())
895             return Promise.resolve();
896
897         return new Promise(resolve => testRunner.runUIScript(`uiController.setViewScale(${scale})`, resolve));
898     }
899
900     static resignFirstResponder()
901     {
902         if (!this.isWebKit2())
903             return Promise.resolve();
904
905         return new Promise(resolve => testRunner.runUIScript(`uiController.resignFirstResponder()`, resolve));
906     }
907
908     static minimumZoomScale()
909     {
910         if (!this.isWebKit2())
911             return Promise.resolve();
912
913         return new Promise(resolve => {
914             testRunner.runUIScript(`(() => {
915                 uiController.uiScriptComplete(uiController.minimumZoomScale);
916             })()`, scaleAsString => resolve(parseFloat(scaleAsString)))
917         });
918     }
919
920     static drawSquareInEditableImage()
921     {
922         if (!this.isWebKit2())
923             return Promise.resolve();
924
925         return new Promise(resolve => testRunner.runUIScript(`uiController.drawSquareInEditableImage()`, resolve));
926     }
927
928     static stylusTapAt(x, y, modifiers=[])
929     {
930         if (!this.isWebKit2())
931             return Promise.resolve();
932
933         return new Promise((resolve) => {
934             testRunner.runUIScript(`
935                 uiController.stylusTapAtPointWithModifiers(${x}, ${y}, 2, 1, 0.5, ${JSON.stringify(modifiers)}, function() {
936                     uiController.uiScriptComplete();
937                 });`, resolve);
938         });
939     }
940
941     static numberOfStrokesInEditableImage()
942     {
943         if (!this.isWebKit2())
944             return Promise.resolve();
945
946         return new Promise(resolve => {
947             testRunner.runUIScript(`(() => {
948                 uiController.uiScriptComplete(uiController.numberOfStrokesInEditableImage);
949             })()`, numberAsString => resolve(parseInt(numberAsString, 10)))
950         });
951     }
952
953     static attachmentInfo(attachmentIdentifier)
954     {
955         if (!this.isWebKit2())
956             return Promise.resolve();
957
958         return new Promise(resolve => {
959             testRunner.runUIScript(`(() => {
960                 uiController.uiScriptComplete(JSON.stringify(uiController.attachmentInfo('${attachmentIdentifier}')));
961             })()`, jsonString => {
962                 resolve(JSON.parse(jsonString));
963             })
964         });
965     }
966
967     static setMinimumEffectiveWidth(effectiveWidth)
968     {
969         if (!this.isWebKit2())
970             return Promise.resolve();
971
972         return new Promise(resolve => testRunner.runUIScript(`uiController.setMinimumEffectiveWidth(${effectiveWidth})`, resolve));
973     }
974
975     static setAllowsViewportShrinkToFit(allows)
976     {
977         if (!this.isWebKit2())
978             return Promise.resolve();
979
980         return new Promise(resolve => testRunner.runUIScript(`uiController.setAllowsViewportShrinkToFit(${allows})`, resolve));
981     }
982
983     static setKeyboardInputModeIdentifier(identifier)
984     {
985         if (!this.isWebKit2())
986             return Promise.resolve();
987
988         const escapedIdentifier = identifier.replace(/`/g, "\\`");
989         return new Promise(resolve => testRunner.runUIScript(`uiController.setKeyboardInputModeIdentifier(\`${escapedIdentifier}\`)`, resolve));
990     }
991
992     static contentOffset()
993     {
994         if (!this.isIOSFamily())
995             return Promise.resolve();
996
997         const uiScript = "JSON.stringify([uiController.contentOffsetX, uiController.contentOffsetY])";
998         return new Promise(resolve => testRunner.runUIScript(uiScript, result => {
999             const [offsetX, offsetY] = JSON.parse(result)
1000             resolve({ x: offsetX, y: offsetY });
1001         }));
1002     }
1003
1004     static undoAndRedoLabels()
1005     {
1006         if (!this.isWebKit2())
1007             return Promise.resolve();
1008
1009         const script = "JSON.stringify([uiController.lastUndoLabel, uiController.firstRedoLabel])";
1010         return new Promise(resolve => testRunner.runUIScript(script, result => resolve(JSON.parse(result))));
1011     }
1012
1013     static waitForMenuToShow()
1014     {
1015         return new Promise(resolve => {
1016             testRunner.runUIScript(`
1017                 (function() {
1018                     if (!uiController.isShowingMenu)
1019                         uiController.didShowMenuCallback = () => uiController.uiScriptComplete();
1020                     else
1021                         uiController.uiScriptComplete();
1022                 })()`, resolve);
1023         });
1024     }
1025
1026     static waitForMenuToHide()
1027     {
1028         return new Promise(resolve => {
1029             testRunner.runUIScript(`
1030                 (function() {
1031                     if (uiController.isShowingMenu)
1032                         uiController.didHideMenuCallback = () => uiController.uiScriptComplete();
1033                     else
1034                         uiController.uiScriptComplete();
1035                 })()`, resolve);
1036         });
1037     }
1038
1039     static isShowingMenu()
1040     {
1041         return new Promise(resolve => {
1042             testRunner.runUIScript(`uiController.isShowingMenu`, result => resolve(result === "true"));
1043         });
1044     }
1045
1046     static isDismissingMenu()
1047     {
1048         return new Promise(resolve => {
1049             testRunner.runUIScript(`uiController.isDismissingMenu`, result => resolve(result === "true"));
1050         });
1051     }
1052
1053     static menuRect()
1054     {
1055         return new Promise(resolve => {
1056             testRunner.runUIScript("JSON.stringify(uiController.menuRect)", result => resolve(JSON.parse(result)));
1057         });
1058     }
1059
1060     static setHardwareKeyboardAttached(attached)
1061     {
1062         return new Promise(resolve => testRunner.runUIScript(`uiController.setHardwareKeyboardAttached(${attached ? "true" : "false"})`, resolve));
1063     }
1064
1065     static rectForMenuAction(action)
1066     {
1067         return new Promise(resolve => {
1068             testRunner.runUIScript(`
1069                 (() => {
1070                     const rect = uiController.rectForMenuAction("${action}");
1071                     uiController.uiScriptComplete(rect ? JSON.stringify(rect) : "");
1072                 })();
1073             `, stringResult => {
1074                 resolve(stringResult.length ? JSON.parse(stringResult) : null);
1075             });
1076         });
1077     }
1078
1079     static async chooseMenuAction(action)
1080     {
1081         const menuRect = await this.rectForMenuAction(action);
1082         if (menuRect)
1083             await this.activateAt(menuRect.left + menuRect.width / 2, menuRect.top + menuRect.height / 2);
1084     }
1085
1086     static waitForEvent(target, eventName)
1087     {
1088         return new Promise(resolve => target.addEventListener(eventName, resolve, { once: true }));
1089     }
1090
1091     static callFunctionAndWaitForEvent(functionToCall, target, eventName)
1092     {
1093         return new Promise((resolve) => {
1094             target.addEventListener(eventName, resolve, { once: true });
1095             functionToCall();
1096         });
1097     }
1098
1099     static callFunctionAndWaitForScrollToFinish(functionToCall, ...theArguments)
1100     {
1101         return UIHelper.callFunctionAndWaitForTargetScrollToFinish(window, functionToCall, theArguments)
1102     }
1103
1104     static callFunctionAndWaitForTargetScrollToFinish(scrollTarget, functionToCall, ...theArguments)
1105     {
1106         return new Promise((resolved) => {
1107             function scrollDidFinish() {
1108                 scrollTarget.removeEventListener("scroll", handleScroll, true);
1109                 resolved();
1110             }
1111
1112             let lastScrollTimerId = 0; // When the timer with this id fires then the page has finished scrolling.
1113             function handleScroll() {
1114                 if (lastScrollTimerId) {
1115                     window.clearTimeout(lastScrollTimerId);
1116                     lastScrollTimerId = 0;
1117                 }
1118                 lastScrollTimerId = window.setTimeout(scrollDidFinish, 300); // Over 250ms to give some room for error.
1119             }
1120             scrollTarget.addEventListener("scroll", handleScroll, true);
1121
1122             functionToCall.apply(this, theArguments);
1123         });
1124     }
1125
1126     static rotateDevice(orientationName, animatedResize = false)
1127     {
1128         if (!this.isWebKit2() || !this.isIOSFamily())
1129             return Promise.resolve();
1130
1131         return new Promise(resolve => {
1132             testRunner.runUIScript(`(() => {
1133                 uiController.${animatedResize ? "simulateRotationLikeSafari" : "simulateRotation"}("${orientationName}", function() {
1134                     uiController.doAfterVisibleContentRectUpdate(() => uiController.uiScriptComplete());
1135                 });
1136             })()`, resolve);
1137         });
1138     }
1139
1140     static getScrollingTree()
1141     {
1142         if (!this.isWebKit2() || !this.isIOSFamily())
1143             return Promise.resolve();
1144
1145         return new Promise(resolve => {
1146             testRunner.runUIScript(`(() => {
1147                 return uiController.scrollingTreeAsText;
1148             })()`, resolve);
1149         });
1150     }
1151
1152     static dragFromPointToPoint(fromX, fromY, toX, toY, duration)
1153     {
1154         if (!this.isWebKit2() || !this.isIOSFamily()) {
1155             eventSender.mouseMoveTo(fromX, fromY);
1156             eventSender.mouseDown();
1157             eventSender.leapForward(duration * 1000);
1158             eventSender.mouseMoveTo(toX, toY);
1159             eventSender.mouseUp();
1160             return Promise.resolve();
1161         }
1162
1163         return new Promise(resolve => {
1164             testRunner.runUIScript(`(() => {
1165                 uiController.dragFromPointToPoint(${fromX}, ${fromY}, ${toX}, ${toY}, ${duration}, () => {
1166                     uiController.uiScriptComplete();
1167                 });
1168             })();`, resolve);
1169         });
1170     }
1171
1172     static waitForDoubleTapDelay()
1173     {
1174         const uiScript = `uiController.doAfterDoubleTapDelay(() => uiController.uiScriptComplete(""))`;
1175         return new Promise(resolve => testRunner.runUIScript(uiScript, resolve));
1176     }
1177
1178     static async waitForSelectionToAppear() {
1179         while (true) {
1180             if ((await this.getUISelectionViewRects()).length > 0)
1181                 break;
1182         }
1183     }
1184
1185     static async waitForSelectionToDisappear() {
1186         while (true) {
1187             if (!(await this.getUISelectionViewRects()).length)
1188                 break;
1189         }
1190     }
1191
1192     static async copyText(text) {
1193         const copyTextScript = `uiController.copyText(\`${text.replace(/`/g, "\\`")}\`)`;
1194         return new Promise(resolve => testRunner.runUIScript(copyTextScript, resolve));
1195     }
1196
1197     static async paste() {
1198         return new Promise(resolve => testRunner.runUIScript(`uiController.paste()`, resolve));
1199     }
1200
1201     static async setContinuousSpellCheckingEnabled(enabled) {
1202         return new Promise(resolve => {
1203             testRunner.runUIScript(`uiController.setContinuousSpellCheckingEnabled(${enabled})`, resolve);
1204         });
1205     }
1206
1207     static async longPressElement(element)
1208     {
1209         return this.longPressAtPoint(element.offsetLeft + element.offsetWidth / 2, element.offsetTop + element.offsetHeight / 2);
1210     }
1211
1212     static async longPressAtPoint(x, y)
1213     {
1214         return new Promise(resolve => {
1215             testRunner.runUIScript(`
1216                 (function() {
1217                     uiController.longPressAtPoint(${x}, ${y}, function() {
1218                         uiController.uiScriptComplete();
1219                     });
1220                 })();`, resolve);
1221         });
1222     }
1223
1224     static async activateElementAfterInstallingTapGestureOnWindow(element)
1225     {
1226         if (!this.isWebKit2() || !this.isIOSFamily())
1227             return activateElement(element);
1228
1229         const x = element.offsetLeft + element.offsetWidth / 2;
1230         const y = element.offsetTop + element.offsetHeight / 2;
1231         return new Promise(resolve => {
1232             testRunner.runUIScript(`
1233                 (function() {
1234                     let progress = 0;
1235                     function incrementProgress() {
1236                         if (++progress == 2)
1237                             uiController.uiScriptComplete();
1238                     }
1239                     uiController.installTapGestureOnWindow(incrementProgress);
1240                     uiController.singleTapAtPoint(${x}, ${y}, incrementProgress);
1241                 })();`, resolve);
1242         });
1243     }
1244
1245     static mayContainEditableElementsInRect(x, y, width, height)
1246     {
1247         if (!this.isWebKit2() || !this.isIOSFamily())
1248             return Promise.resolve(false);
1249
1250         return new Promise(resolve => {
1251             testRunner.runUIScript(`
1252                 (function() {
1253                     uiController.doAfterPresentationUpdate(function() {
1254                         uiController.uiScriptComplete(uiController.mayContainEditableElementsInRect(${x}, ${y}, ${width}, ${height}));
1255                     })
1256                 })();`, result => resolve(result === "true"));
1257         });
1258     }
1259 }
1260
1261 UIHelper.EventStreamBuilder = class {
1262     constructor()
1263     {
1264         // FIXME: This could support additional customization options, such as interpolation, timestep, and different
1265         // digitizer indices in the future. For now, just make it simpler to string together sequences of pan gestures.
1266         this._reset();
1267     }
1268
1269     _reset() {
1270         this.events = [];
1271         this.currentTimeOffset = 0;
1272         this.currentX = 0;
1273         this.currentY = 0;
1274     }
1275
1276     begin(x, y) {
1277         console.assert(this.currentTimeOffset === 0);
1278         this.events.push({
1279             interpolate : "linear",
1280             timestep : 0.016,
1281             coordinateSpace : "content",
1282             startEvent : {
1283                 inputType : "hand",
1284                 timeOffset : this.currentTimeOffset,
1285                 touches : [{ inputType : "finger", phase : "began", id : 1, x : x, y : y, pressure : 0 }]
1286             },
1287             endEvent : {
1288                 inputType : "hand",
1289                 timeOffset : this.currentTimeOffset,
1290                 touches : [{ inputType : "finger", phase : "began", id : 1, x : x, y : y, pressure : 0 }]
1291             }
1292         });
1293         this.currentX = x;
1294         this.currentY = y;
1295         return this;
1296     }
1297
1298     move(x, y, duration = 0) {
1299         const previousTimeOffset = this.currentTimeOffset;
1300         this.currentTimeOffset += duration;
1301         this.events.push({
1302             interpolate : "linear",
1303             timestep : 0.016,
1304             coordinateSpace : "content",
1305             startEvent : {
1306                 inputType : "hand",
1307                 timeOffset : previousTimeOffset,
1308                 touches : [{ inputType : "finger", phase : "moved", id : 1, x : this.currentX, y : this.currentY, pressure : 0 }]
1309             },
1310             endEvent : {
1311                 inputType : "hand",
1312                 timeOffset : this.currentTimeOffset,
1313                 touches : [{ inputType : "finger", phase : "moved", id : 1, x : x, y : y, pressure : 0 }]
1314             }
1315         });
1316         this.currentX = x;
1317         this.currentY = y;
1318         return this;
1319     }
1320
1321     end() {
1322         this.events.push({
1323             interpolate : "linear",
1324             timestep : 0.016,
1325             coordinateSpace : "content",
1326             startEvent : {
1327                 inputType : "hand",
1328                 timeOffset : this.currentTimeOffset,
1329                 touches : [{ inputType : "finger", phase : "ended", id : 1, x : this.currentX, y : this.currentY, pressure : 0 }]
1330             },
1331             endEvent : {
1332                 inputType : "hand",
1333                 timeOffset : this.currentTimeOffset,
1334                 touches : [{ inputType : "finger", phase : "ended", id : 1, x : this.currentX, y : this.currentY, pressure : 0 }]
1335             }
1336         });
1337         return this;
1338     }
1339
1340     takeResult() {
1341         const events = this.events;
1342         this._reset();
1343         return { "events": events };
1344     }
1345 }