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