• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1(function() {
2    "use strict";
3    var idCounter = 0;
4
5    function getInViewCenterPoint(rect) {
6        var left = Math.max(0, rect.left);
7        var right = Math.min(window.innerWidth, rect.right);
8        var top = Math.max(0, rect.top);
9        var bottom = Math.min(window.innerHeight, rect.bottom);
10
11        var x = 0.5 * (left + right);
12        var y = 0.5 * (top + bottom);
13
14        return [x, y];
15    }
16
17    function getPointerInteractablePaintTree(element) {
18        if (!window.document.contains(element)) {
19            return [];
20        }
21
22        var rectangles = element.getClientRects();
23
24        if (rectangles.length === 0) {
25            return [];
26        }
27
28        var centerPoint = getInViewCenterPoint(rectangles[0]);
29
30        if ("elementsFromPoint" in document) {
31            return document.elementsFromPoint(centerPoint[0], centerPoint[1]);
32        } else if ("msElementsFromPoint" in document) {
33            var rv = document.msElementsFromPoint(centerPoint[0], centerPoint[1]);
34            return Array.prototype.slice.call(rv ? rv : []);
35        } else {
36            throw new Error("document.elementsFromPoint unsupported");
37        }
38    }
39
40    function inView(element) {
41        var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
42        return pointerInteractablePaintTree.indexOf(element) !== -1;
43    }
44
45
46    /**
47     * @namespace
48     */
49    window.test_driver = {
50        /**
51         * Trigger user interaction in order to grant additional privileges to
52         * a provided function.
53         *
54         * https://html.spec.whatwg.org/#triggered-by-user-activation
55         *
56         * @param {String} intent - a description of the action which much be
57         *                          triggered by user interaction
58         * @param {Function} action - code requiring escalated privileges
59         *
60         * @returns {Promise} fulfilled following user interaction and
61         *                    execution of the provided `action` function;
62         *                    rejected if interaction fails or the provided
63         *                    function throws an error
64         */
65        bless: function(intent, action) {
66            var button = document.createElement("button");
67            button.innerHTML = "This test requires user interaction.<br />" +
68                "Please click here to allow " + intent + ".";
69            button.id = "wpt-test-driver-bless-" + (idCounter += 1);
70            const elem = document.body || document.documentElement;
71            elem.appendChild(button);
72
73            return new Promise(function(resolve, reject) {
74                    button.addEventListener("click", resolve);
75
76                    test_driver.click(button).catch(reject);
77                }).then(function() {
78                    button.remove();
79
80                    if (typeof action === "function") {
81                        return action();
82                    }
83                });
84        },
85
86        /**
87         * Triggers a user-initiated click
88         *
89         * This matches the behaviour of the {@link
90         * https://w3c.github.io/webdriver/webdriver-spec.html#element-click|WebDriver
91         * Element Click command}.
92         *
93         * @param {Element} element - element to be clicked
94         * @returns {Promise} fulfilled after click occurs, or rejected in
95         *                    the cases the WebDriver command errors
96         */
97        click: function(element) {
98            if (window.top !== window) {
99                return Promise.reject(new Error("can only click in top-level window"));
100            }
101
102            if (!window.document.contains(element)) {
103                return Promise.reject(new Error("element in different document or shadow tree"));
104            }
105
106            if (!inView(element)) {
107                element.scrollIntoView({behavior: "instant",
108                                        block: "end",
109                                        inline: "nearest"});
110            }
111
112            var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
113            if (pointerInteractablePaintTree.length === 0 ||
114                !element.contains(pointerInteractablePaintTree[0])) {
115                return Promise.reject(new Error("element click intercepted error"));
116            }
117
118            var rect = element.getClientRects()[0];
119            var centerPoint = getInViewCenterPoint(rect);
120            return window.test_driver_internal.click(element,
121                                                     {x: centerPoint[0],
122                                                      y: centerPoint[1]});
123        },
124
125        /**
126         * Send keys to an element
127         *
128         * This matches the behaviour of the {@link
129         * https://w3c.github.io/webdriver/webdriver-spec.html#element-send-keys|WebDriver
130         * Send Keys command}.
131         *
132         * @param {Element} element - element to send keys to
133         * @param {String} keys - keys to send to the element
134         * @returns {Promise} fulfilled after keys are sent, or rejected in
135         *                    the cases the WebDriver command errors
136         */
137        send_keys: function(element, keys) {
138            if (window.top !== window) {
139                return Promise.reject(new Error("can only send keys in top-level window"));
140            }
141
142            if (!window.document.contains(element)) {
143                return Promise.reject(new Error("element in different document or shadow tree"));
144            }
145
146            if (!inView(element)) {
147                element.scrollIntoView({behavior: "instant",
148                                        block: "end",
149                                        inline: "nearest"});
150            }
151
152            var pointerInteractablePaintTree = getPointerInteractablePaintTree(element);
153            if (pointerInteractablePaintTree.length === 0 ||
154                !element.contains(pointerInteractablePaintTree[0])) {
155                return Promise.reject(new Error("element send_keys intercepted error"));
156            }
157
158            return window.test_driver_internal.send_keys(element, keys);
159        },
160
161        /**
162         * Freeze the current page
163         *
164         * The freeze function transitions the page from the HIDDEN state to
165         * the FROZEN state as described in {@link
166         * https://github.com/WICG/page-lifecycle/blob/master/README.md|Lifecycle API
167         * for Web Pages}
168         *
169         * @returns {Promise} fulfilled after the freeze request is sent, or rejected
170         *                    in case the WebDriver command errors
171         */
172        freeze: function() {
173            return window.test_driver_internal.freeze();
174        },
175
176        /**
177         * Send a sequence of actions
178         *
179         * This function sends a sequence of actions to the top level window
180         * to perform. It is modeled after the behaviour of {@link
181         * https://w3c.github.io/webdriver/#actions|WebDriver Actions Command}
182         *
183         * @param {Array} actions - an array of actions. The format is the same as the actions
184                                    property of the WebDriver command {@link
185                                    https://w3c.github.io/webdriver/#perform-actions|Perform
186                                    Actions} command. Each element is an object representing an
187                                    input source and each input source itself has an actions
188                                    property detailing the behaviour of that source at each timestep
189                                    (or tick). Authors are not expected to construct the actions
190                                    sequence by hand, but to use the builder api provided in
191                                    testdriver-actions.js
192         * @returns {Promise} fufiled after the actions are performed, or rejected in
193         *                    the cases the WebDriver command errors
194         */
195        action_sequence: function(actions) {
196            return window.test_driver_internal.action_sequence(actions);
197        },
198
199        /**
200         * Generates a test report on the current page
201         *
202         * The generate_test_report function generates a report (to be observed
203         * by ReportingObserver) for testing purposes, as described in
204         * {@link https://w3c.github.io/reporting/#generate-test-report-command}
205         *
206         * @returns {Promise} fulfilled after the report is generated, or
207         *                    rejected if the report generation fails
208         */
209        generate_test_report: function(message) {
210            return window.test_driver_internal.generate_test_report(message);
211        }
212    };
213
214    window.test_driver_internal = {
215        /**
216         * This flag should be set to `true` by any code which implements the
217         * internal methods defined below for automation purposes. Doing so
218         * allows the library to signal failure immediately when an automated
219         * implementation of one of the methods is not available.
220         */
221        in_automation: false,
222
223        /**
224         * Waits for a user-initiated click
225         *
226         * @param {Element} element - element to be clicked
227         * @param {{x: number, y: number} coords - viewport coordinates to click at
228         * @returns {Promise} fulfilled after click occurs
229         */
230        click: function(element, coords) {
231            if (this.in_automation) {
232                return Promise.reject(new Error('Not implemented'));
233            }
234
235            return new Promise(function(resolve, reject) {
236                element.addEventListener("click", resolve);
237            });
238        },
239
240        /**
241         * Waits for an element to receive a series of key presses
242         *
243         * @param {Element} element - element which should receve key presses
244         * @param {String} keys - keys to expect
245         * @returns {Promise} fulfilled after keys are received or rejected if
246         *                    an incorrect key sequence is received
247         */
248        send_keys: function(element, keys) {
249            if (this.in_automation) {
250                return Promise.reject(new Error('Not implemented'));
251            }
252
253            return new Promise(function(resolve, reject) {
254                var seen = "";
255
256                function remove() {
257                    element.removeEventListener("keydown", onKeyDown);
258                }
259
260                function onKeyDown(event) {
261                    if (event.key.length > 1) {
262                        return;
263                    }
264
265                    seen += event.key;
266
267                    if (keys.indexOf(seen) !== 0) {
268                        reject(new Error("Unexpected key sequence: " + seen));
269                        remove();
270                    } else if (seen === keys) {
271                        resolve();
272                        remove();
273                    }
274                }
275
276                element.addEventListener("keydown", onKeyDown);
277            });
278        },
279
280        /**
281         * Freeze the current page
282         *
283         * @returns {Promise} fulfilled after freeze request is sent, otherwise
284         * it gets rejected
285         */
286        freeze: function() {
287            return Promise.reject(new Error("unimplemented"));
288        },
289
290        /**
291         * Send a sequence of pointer actions
292         *
293         * @returns {Promise} fufilled after actions are sent, rejected if any actions
294         *                    fail
295         */
296        action_sequence: function(actions) {
297            return Promise.reject(new Error("unimplemented"));
298        },
299
300        /**
301         * Generates a test report on the current page
302         *
303         * @param {String} message - the message to be contained in the report
304         * @returns {Promise} fulfilled after the report is generated, or
305         *                    rejected if the report generation fails
306         */
307        generate_test_report: function(message) {
308            return Promise.reject(new Error("unimplemented"));
309        }
310    };
311})();
312