1(function() { 2 "use strict"; 3 var idCounter = 0; 4 let testharness_context = null; 5 6 function getInViewCenterPoint(rect) { 7 var left = Math.max(0, rect.left); 8 var right = Math.min(window.innerWidth, rect.right); 9 var top = Math.max(0, rect.top); 10 var bottom = Math.min(window.innerHeight, rect.bottom); 11 12 var x = 0.5 * (left + right); 13 var y = 0.5 * (top + bottom); 14 15 return [x, y]; 16 } 17 18 function getPointerInteractablePaintTree(element) { 19 let elementDocument = element.ownerDocument; 20 if (!elementDocument.contains(element)) { 21 return []; 22 } 23 24 var rectangles = element.getClientRects(); 25 26 if (rectangles.length === 0) { 27 return []; 28 } 29 30 var centerPoint = getInViewCenterPoint(rectangles[0]); 31 32 if ("elementsFromPoint" in elementDocument) { 33 return elementDocument.elementsFromPoint(centerPoint[0], centerPoint[1]); 34 } else if ("msElementsFromPoint" in elementDocument) { 35 var rv = elementDocument.msElementsFromPoint(centerPoint[0], centerPoint[1]); 36 return Array.prototype.slice.call(rv ? rv : []); 37 } else { 38 throw new Error("document.elementsFromPoint unsupported"); 39 } 40 } 41 42 function inView(element) { 43 var pointerInteractablePaintTree = getPointerInteractablePaintTree(element); 44 return pointerInteractablePaintTree.indexOf(element) !== -1; 45 } 46 47 48 /** 49 * @namespace {test_driver} 50 */ 51 window.test_driver = { 52 /** 53 * Set the context in which testharness.js is loaded 54 * 55 * @param {WindowProxy} context - the window containing testharness.js 56 **/ 57 set_test_context: function(context) { 58 if (window.test_driver_internal.set_test_context) { 59 window.test_driver_internal.set_test_context(context); 60 } 61 testharness_context = context; 62 }, 63 64 /** 65 * postMessage to the context containing testharness.js 66 * 67 * @param {Object} msg - the data to POST 68 **/ 69 message_test: function(msg) { 70 let target = testharness_context; 71 if (testharness_context === null) { 72 target = window; 73 } 74 target.postMessage(msg, "*"); 75 }, 76 77 /** 78 * Trigger user interaction in order to grant additional privileges to 79 * a provided function. 80 * 81 * See `triggered by user activation 82 * <https://html.spec.whatwg.org/#triggered-by-user-activation>`_. 83 * 84 * @example 85 * var mediaElement = document.createElement('video'); 86 * 87 * test_driver.bless('initiate media playback', function () { 88 * mediaElement.play(); 89 * }); 90 * 91 * @param {String} intent - a description of the action which must be 92 * triggered by user interaction 93 * @param {Function} action - code requiring escalated privileges 94 * @param {WindowProxy} context - Browsing context in which 95 * to run the call, or null for the current 96 * browsing context. 97 * 98 * @returns {Promise} fulfilled following user interaction and 99 * execution of the provided `action` function; 100 * rejected if interaction fails or the provided 101 * function throws an error 102 */ 103 bless: function(intent, action, context=null) { 104 let contextDocument = context ? context.document : document; 105 var button = contextDocument.createElement("button"); 106 button.innerHTML = "This test requires user interaction.<br />" + 107 "Please click here to allow " + intent + "."; 108 button.id = "wpt-test-driver-bless-" + (idCounter += 1); 109 const elem = contextDocument.body || contextDocument.documentElement; 110 elem.appendChild(button); 111 112 let wait_click = new Promise(resolve => button.addEventListener("click", resolve)); 113 114 return test_driver.click(button) 115 .then(wait_click) 116 .then(function() { 117 button.remove(); 118 119 if (typeof action === "function") { 120 return action(); 121 } 122 return null; 123 }); 124 }, 125 126 /** 127 * Triggers a user-initiated click 128 * 129 * If ``element`` isn't inside the 130 * viewport, it will be scrolled into view before the click 131 * occurs. 132 * 133 * If ``element`` is from a different browsing context, the 134 * command will be run in that context. 135 * 136 * Matches the behaviour of the `Element Click 137 * <https://w3c.github.io/webdriver/#element-click>`_ 138 * WebDriver command. 139 * 140 * **Note:** If the element to be clicked does not have a 141 * unique ID, the document must not have any DOM mutations 142 * made between the function being called and the promise 143 * settling. 144 * 145 * @param {Element} element - element to be clicked 146 * @returns {Promise} fulfilled after click occurs, or rejected in 147 * the cases the WebDriver command errors 148 */ 149 click: function(element) { 150 if (!inView(element)) { 151 element.scrollIntoView({behavior: "instant", 152 block: "end", 153 inline: "nearest"}); 154 } 155 156 var pointerInteractablePaintTree = getPointerInteractablePaintTree(element); 157 if (pointerInteractablePaintTree.length === 0 || 158 !element.contains(pointerInteractablePaintTree[0])) { 159 return Promise.reject(new Error("element click intercepted error")); 160 } 161 162 var rect = element.getClientRects()[0]; 163 var centerPoint = getInViewCenterPoint(rect); 164 return window.test_driver_internal.click(element, 165 {x: centerPoint[0], 166 y: centerPoint[1]}); 167 }, 168 169 /** 170 * Deletes all cookies. 171 * 172 * Matches the behaviour of the `Delete All Cookies 173 * <https://w3c.github.io/webdriver/#delete-all-cookies>`_ 174 * WebDriver command. 175 * 176 * @param {WindowProxy} context - Browsing context in which 177 * to run the call, or null for the current 178 * browsing context. 179 * 180 * @returns {Promise} fulfilled after cookies are deleted, or rejected in 181 * the cases the WebDriver command errors 182 */ 183 delete_all_cookies: function(context=null) { 184 return window.test_driver_internal.delete_all_cookies(context); 185 }, 186 187 /** 188 * Send keys to an element. 189 * 190 * If ``element`` isn't inside the 191 * viewport, it will be scrolled into view before the click 192 * occurs. 193 * 194 * If ``element`` is from a different browsing context, the 195 * command will be run in that context. 196 * 197 * To send special keys, send the respective key's codepoint, 198 * as defined by `WebDriver 199 * <https://w3c.github.io/webdriver/#keyboard-actions>`_. For 200 * example, the "tab" key is represented as "``\uE004``". 201 * 202 * **Note:** these special-key codepoints are not necessarily 203 * what you would expect. For example, <kbd>Esc</kbd> is the 204 * invalid Unicode character ``\uE00C``, not the ``\u001B`` Escape 205 * character from ASCII. 206 * 207 * This matches the behaviour of the 208 * `Send Keys 209 * <https://w3c.github.io/webdriver/#element-send-keys>`_ 210 * WebDriver command. 211 * 212 * **Note:** If the element to be clicked does not have a 213 * unique ID, the document must not have any DOM mutations 214 * made between the function being called and the promise 215 * settling. 216 * 217 * @param {Element} element - element to send keys to 218 * @param {String} keys - keys to send to the element 219 * @returns {Promise} fulfilled after keys are sent, or rejected in 220 * the cases the WebDriver command errors 221 */ 222 send_keys: function(element, keys) { 223 if (!inView(element)) { 224 element.scrollIntoView({behavior: "instant", 225 block: "end", 226 inline: "nearest"}); 227 } 228 229 var pointerInteractablePaintTree = getPointerInteractablePaintTree(element); 230 if (pointerInteractablePaintTree.length === 0 || 231 !element.contains(pointerInteractablePaintTree[0])) { 232 return Promise.reject(new Error("element send_keys intercepted error")); 233 } 234 235 return window.test_driver_internal.send_keys(element, keys); 236 }, 237 238 /** 239 * Freeze the current page 240 * 241 * The freeze function transitions the page from the HIDDEN state to 242 * the FROZEN state as described in `Lifecycle API for Web Pages 243 * <https://github.com/WICG/page-lifecycle/blob/master/README.md>`_. 244 * 245 * @param {WindowProxy} context - Browsing context in which 246 * to run the call, or null for the current 247 * browsing context. 248 * 249 * @returns {Promise} fulfilled after the freeze request is sent, or rejected 250 * in case the WebDriver command errors 251 */ 252 freeze: function(context=null) { 253 return window.test_driver_internal.freeze(); 254 }, 255 256 /** 257 * Minimizes the browser window. 258 * 259 * Matches the the behaviour of the `Minimize 260 * <https://www.w3.org/TR/webdriver/#minimize-window>`_ 261 * WebDriver command 262 * 263 * @param {WindowProxy} context - Browsing context in which 264 * to run the call, or null for the current 265 * browsing context. 266 * 267 * @returns {Promise} fulfilled with the previous {@link 268 * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect} 269 * value, after the window is minimized. 270 */ 271 minimize_window: function(context=null) { 272 return window.test_driver_internal.minimize_window(context); 273 }, 274 275 /** 276 * Restore the window from minimized/maximized state to a given rect. 277 * 278 * Matches the behaviour of the `Set Window Rect 279 * <https://www.w3.org/TR/webdriver/#set-window-rect>`_ 280 * WebDriver command 281 * 282 * @param {Object} rect - A {@link 283 * https://www.w3.org/TR/webdriver/#dfn-windowrect-object|WindowRect} 284 * @param {WindowProxy} context - Browsing context in which 285 * to run the call, or null for the current 286 * browsing context. 287 * 288 * @returns {Promise} fulfilled after the window is restored to the given rect. 289 */ 290 set_window_rect: function(rect, context=null) { 291 return window.test_driver_internal.set_window_rect(rect, context); 292 }, 293 294 /** 295 * Send a sequence of actions 296 * 297 * This function sends a sequence of actions to perform. 298 * 299 * Matches the behaviour of the `Actions 300 * <https://w3c.github.io/webdriver/#actions>`_ feature in 301 * WebDriver. 302 * 303 * Authors are encouraged to use the 304 * :js:class:`test_driver.Actions` builder rather than 305 * invoking this API directly. 306 * 307 * @param {Array} actions - an array of actions. The format is 308 * the same as the actions property 309 * of the `Perform Actions 310 * <https://w3c.github.io/webdriver/#perform-actions>`_ 311 * WebDriver command. Each element is 312 * an object representing an input 313 * source and each input source 314 * itself has an actions property 315 * detailing the behaviour of that 316 * source at each timestep (or 317 * tick). Authors are not expected to 318 * construct the actions sequence by 319 * hand, but to use the builder api 320 * provided in testdriver-actions.js 321 * @param {WindowProxy} context - Browsing context in which 322 * to run the call, or null for the current 323 * browsing context. 324 * 325 * @returns {Promise} fulfilled after the actions are performed, or rejected in 326 * the cases the WebDriver command errors 327 */ 328 action_sequence: function(actions, context=null) { 329 return window.test_driver_internal.action_sequence(actions, context); 330 }, 331 332 /** 333 * Generates a test report on the current page 334 * 335 * The generate_test_report function generates a report (to be 336 * observed by ReportingObserver) for testing purposes. 337 * 338 * Matches the `Generate Test Report 339 * <https://w3c.github.io/reporting/#generate-test-report-command>`_ 340 * WebDriver command. 341 * 342 * @param {WindowProxy} context - Browsing context in which 343 * to run the call, or null for the current 344 * browsing context. 345 * 346 * @returns {Promise} fulfilled after the report is generated, or 347 * rejected if the report generation fails 348 */ 349 generate_test_report: function(message, context=null) { 350 return window.test_driver_internal.generate_test_report(message, context); 351 }, 352 353 /** 354 * Sets the state of a permission 355 * 356 * This function simulates a user setting a permission into a 357 * particular state. 358 * 359 * Matches the `Set Permission 360 * <https://w3c.github.io/permissions/#set-permission-command>`_ 361 * WebDriver command. 362 * 363 * @example 364 * await test_driver.set_permission({ name: "background-fetch" }, "denied"); 365 * await test_driver.set_permission({ name: "push", userVisibleOnly: true }, "granted", true); 366 * 367 * @param {Object} descriptor - a `PermissionDescriptor 368 * <https://w3c.github.io/permissions/#dictdef-permissiondescriptor>`_ 369 * object 370 * @param {String} state - the state of the permission 371 * @param {boolean} one_realm - Optional. Whether the permission applies to only one realm 372 * @param {WindowProxy} context - Browsing context in which 373 * to run the call, or null for the current 374 * browsing context. 375 * @returns {Promise} fulfilled after the permission is set, or rejected if setting the 376 * permission fails 377 */ 378 set_permission: function(descriptor, state, one_realm=false, context=null) { 379 let permission_params = { 380 descriptor, 381 state, 382 oneRealm: one_realm, 383 }; 384 return window.test_driver_internal.set_permission(permission_params, context); 385 }, 386 387 /** 388 * Creates a virtual authenticator 389 * 390 * This function creates a virtual authenticator for use with 391 * the U2F and WebAuthn APIs. 392 * 393 * Matches the `Add Virtual Authenticator 394 * <https://w3c.github.io/webauthn/#sctn-automation-add-virtual-authenticator>`_ 395 * WebDriver command. 396 * 397 * @param {Object} config - an `Authenticator Configuration 398 * <https://w3c.github.io/webauthn/#authenticator-configuration>`_ 399 * object 400 * @param {WindowProxy} context - Browsing context in which 401 * to run the call, or null for the current 402 * browsing context. 403 * 404 * @returns {Promise} fulfilled after the authenticator is added, or 405 * rejected in the cases the WebDriver command 406 * errors. Returns the ID of the authenticator 407 */ 408 add_virtual_authenticator: function(config, context=null) { 409 return window.test_driver_internal.add_virtual_authenticator(config, context); 410 }, 411 412 /** 413 * Removes a virtual authenticator 414 * 415 * This function removes a virtual authenticator that has been 416 * created by :js:func:`add_virtual_authenticator`. 417 * 418 * Matches the `Remove Virtual Authenticator 419 * <https://w3c.github.io/webauthn/#sctn-automation-remove-virtual-authenticator>`_ 420 * WebDriver command. 421 * 422 * @param {String} authenticator_id - the ID of the authenticator to be 423 * removed. 424 * @param {WindowProxy} context - Browsing context in which 425 * to run the call, or null for the current 426 * browsing context. 427 * 428 * @returns {Promise} fulfilled after the authenticator is removed, or 429 * rejected in the cases the WebDriver command 430 * errors 431 */ 432 remove_virtual_authenticator: function(authenticator_id, context=null) { 433 return window.test_driver_internal.remove_virtual_authenticator(authenticator_id, context); 434 }, 435 436 /** 437 * Adds a credential to a virtual authenticator 438 * 439 * Matches the `Add Credential 440 * <https://w3c.github.io/webauthn/#sctn-automation-add-credential>`_ 441 * WebDriver command. 442 * 443 * @param {String} authenticator_id - the ID of the authenticator 444 * @param {Object} credential - A `Credential Parameters 445 * <https://w3c.github.io/webauthn/#credential-parameters>`_ 446 * object 447 * @param {WindowProxy} context - Browsing context in which 448 * to run the call, or null for the current 449 * browsing context. 450 * 451 * @returns {Promise} fulfilled after the credential is added, or 452 * rejected in the cases the WebDriver command 453 * errors 454 */ 455 add_credential: function(authenticator_id, credential, context=null) { 456 return window.test_driver_internal.add_credential(authenticator_id, credential, context); 457 }, 458 459 /** 460 * Gets all the credentials stored in an authenticator 461 * 462 * This function retrieves all the credentials (added via the U2F API, 463 * WebAuthn, or the add_credential function) stored in a virtual 464 * authenticator 465 * 466 * Matches the `Get Credentials 467 * <https://w3c.github.io/webauthn/#sctn-automation-get-credentials>`_ 468 * WebDriver command. 469 * 470 * @param {String} authenticator_id - the ID of the authenticator 471 * @param {WindowProxy} context - Browsing context in which 472 * to run the call, or null for the current 473 * browsing context. 474 * 475 * @returns {Promise} fulfilled after the credentials are 476 * returned, or rejected in the cases the 477 * WebDriver command errors. Returns an 478 * array of `Credential Parameters 479 * <https://w3c.github.io/webauthn/#credential-parameters>`_ 480 */ 481 get_credentials: function(authenticator_id, context=null) { 482 return window.test_driver_internal.get_credentials(authenticator_id, context=null); 483 }, 484 485 /** 486 * Remove a credential stored in an authenticator 487 * 488 * Matches the `Remove Credential 489 * <https://w3c.github.io/webauthn/#sctn-automation-remove-credential>`_ 490 * WebDriver command. 491 * 492 * @param {String} authenticator_id - the ID of the authenticator 493 * @param {String} credential_id - the ID of the credential 494 * @param {WindowProxy} context - Browsing context in which 495 * to run the call, or null for the current 496 * browsing context. 497 * 498 * @returns {Promise} fulfilled after the credential is removed, or 499 * rejected in the cases the WebDriver command 500 * errors. 501 */ 502 remove_credential: function(authenticator_id, credential_id, context=null) { 503 return window.test_driver_internal.remove_credential(authenticator_id, credential_id, context); 504 }, 505 506 /** 507 * Removes all the credentials stored in a virtual authenticator 508 * 509 * Matches the `Remove All Credentials 510 * <https://w3c.github.io/webauthn/#sctn-automation-remove-all-credentials>`_ 511 * WebDriver command. 512 * 513 * @param {String} authenticator_id - the ID of the authenticator 514 * @param {WindowProxy} context - Browsing context in which 515 * to run the call, or null for the current 516 * browsing context. 517 * 518 * @returns {Promise} fulfilled after the credentials are removed, or 519 * rejected in the cases the WebDriver command 520 * errors. 521 */ 522 remove_all_credentials: function(authenticator_id, context=null) { 523 return window.test_driver_internal.remove_all_credentials(authenticator_id, context); 524 }, 525 526 /** 527 * Sets the User Verified flag on an authenticator 528 * 529 * Sets whether requests requiring user verification will succeed or 530 * fail on a given virtual authenticator 531 * 532 * Matches the `Set User Verified 533 * <https://w3c.github.io/webauthn/#sctn-automation-set-user-verified>`_ 534 * WebDriver command. 535 * 536 * @param {String} authenticator_id - the ID of the authenticator 537 * @param {boolean} uv - the User Verified flag 538 * @param {WindowProxy} context - Browsing context in which 539 * to run the call, or null for the current 540 * browsing context. 541 */ 542 set_user_verified: function(authenticator_id, uv, context=null) { 543 return window.test_driver_internal.set_user_verified(authenticator_id, uv, context); 544 }, 545 546 /** 547 * Sets the storage access rule for an origin when embedded 548 * in a third-party context. 549 * 550 * Matches the `Set Storage Access 551 * <https://privacycg.github.io/storage-access/#set-storage-access-command>`_ 552 * WebDriver command. 553 * 554 * @param {String} origin - A third-party origin to block or allow. 555 * May be "*" to indicate all origins. 556 * @param {String} embedding_origin - an embedding (first-party) origin 557 * on which {origin}'s access should 558 * be blocked or allowed. 559 * May be "*" to indicate all origins. 560 * @param {String} state - The storage access setting. 561 * Must be either "allowed" or "blocked". 562 * @param {WindowProxy} context - Browsing context in which 563 * to run the call, or null for the current 564 * browsing context. 565 * 566 * @returns {Promise} Fulfilled after the storage access rule has been 567 * set, or rejected if setting the rule fails. 568 */ 569 set_storage_access: function(origin, embedding_origin, state, context=null) { 570 if (state !== "allowed" && state !== "blocked") { 571 throw new Error("storage access status must be 'allowed' or 'blocked'"); 572 } 573 const blocked = state === "blocked"; 574 return window.test_driver_internal.set_storage_access(origin, embedding_origin, blocked, context); 575 }, 576 577 /** 578 * Sets the current transaction automation mode for Secure Payment 579 * Confirmation. 580 * 581 * This function places `Secure Payment 582 * Confirmation <https://w3c.github.io/secure-payment-confirmation>`_ into 583 * an automated 'autoaccept' or 'autoreject' mode, to allow testing 584 * without user interaction with the transaction UX prompt. 585 * 586 * Matches the `Set SPC Transaction Mode 587 * <https://w3c.github.io/secure-payment-confirmation/#sctn-automation-set-spc-transaction-mode>`_ 588 * WebDriver command. 589 * 590 * @example 591 * await test_driver.set_spc_transaction_mode("autoaccept"); 592 * test.add_cleanup(() => { 593 * return test_driver.set_spc_transaction_mode("none"); 594 * }); 595 * 596 * // Assumption: `request` is a PaymentRequest with a secure-payment-confirmation 597 * // payment method. 598 * const response = await request.show(); 599 * 600 * @param {String} mode - The `transaction mode 601 * <https://w3c.github.io/secure-payment-confirmation/#enumdef-transactionautomationmode>`_ 602 * to set. Must be one of "``none``", 603 * "``autoaccept``", or 604 * "``autoreject``". 605 * @param {WindowProxy} context - Browsing context in which 606 * to run the call, or null for the current 607 * browsing context. 608 * 609 * @returns {Promise} Fulfilled after the transaction mode has been set, 610 * or rejected if setting the mode fails. 611 */ 612 set_spc_transaction_mode: function(mode, context=null) { 613 return window.test_driver_internal.set_spc_transaction_mode(mode, context); 614 }, 615 }; 616 617 window.test_driver_internal = { 618 /** 619 * This flag should be set to `true` by any code which implements the 620 * internal methods defined below for automation purposes. Doing so 621 * allows the library to signal failure immediately when an automated 622 * implementation of one of the methods is not available. 623 */ 624 in_automation: false, 625 626 click: function(element, coords) { 627 if (this.in_automation) { 628 return Promise.reject(new Error('Not implemented')); 629 } 630 631 return new Promise(function(resolve, reject) { 632 element.addEventListener("click", resolve); 633 }); 634 }, 635 636 delete_all_cookies: function(context=null) { 637 return Promise.reject(new Error("unimplemented")); 638 }, 639 640 send_keys: function(element, keys) { 641 if (this.in_automation) { 642 return Promise.reject(new Error('Not implemented')); 643 } 644 645 return new Promise(function(resolve, reject) { 646 var seen = ""; 647 648 function remove() { 649 element.removeEventListener("keydown", onKeyDown); 650 } 651 652 function onKeyDown(event) { 653 if (event.key.length > 1) { 654 return; 655 } 656 657 seen += event.key; 658 659 if (keys.indexOf(seen) !== 0) { 660 reject(new Error("Unexpected key sequence: " + seen)); 661 remove(); 662 } else if (seen === keys) { 663 resolve(); 664 remove(); 665 } 666 } 667 668 element.addEventListener("keydown", onKeyDown); 669 }); 670 }, 671 672 freeze: function(context=null) { 673 return Promise.reject(new Error("unimplemented")); 674 }, 675 676 minimize_window: function(context=null) { 677 return Promise.reject(new Error("unimplemented")); 678 }, 679 680 set_window_rect: function(rect, context=null) { 681 return Promise.reject(new Error("unimplemented")); 682 }, 683 684 action_sequence: function(actions, context=null) { 685 return Promise.reject(new Error("unimplemented")); 686 }, 687 688 generate_test_report: function(message, context=null) { 689 return Promise.reject(new Error("unimplemented")); 690 }, 691 692 693 set_permission: function(permission_params, context=null) { 694 return Promise.reject(new Error("unimplemented")); 695 }, 696 697 add_virtual_authenticator: function(config, context=null) { 698 return Promise.reject(new Error("unimplemented")); 699 }, 700 701 remove_virtual_authenticator: function(authenticator_id, context=null) { 702 return Promise.reject(new Error("unimplemented")); 703 }, 704 705 add_credential: function(authenticator_id, credential, context=null) { 706 return Promise.reject(new Error("unimplemented")); 707 }, 708 709 get_credentials: function(authenticator_id, context=null) { 710 return Promise.reject(new Error("unimplemented")); 711 }, 712 713 remove_credential: function(authenticator_id, credential_id, context=null) { 714 return Promise.reject(new Error("unimplemented")); 715 }, 716 717 remove_all_credentials: function(authenticator_id, context=null) { 718 return Promise.reject(new Error("unimplemented")); 719 }, 720 721 set_user_verified: function(authenticator_id, uv, context=null) { 722 return Promise.reject(new Error("unimplemented")); 723 }, 724 725 set_storage_access: function(origin, embedding_origin, blocked, context=null) { 726 return Promise.reject(new Error("unimplemented")); 727 }, 728 729 set_spc_transaction_mode: function(mode, context=null) { 730 return Promise.reject(new Error("unimplemented")); 731 }, 732 733 }; 734})(); 735