head.js (26871B)
1 /* Any copyright is dedicated to the Public Domain. 2 http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 /* global waitUntilState, gBrowser */ 5 /* exported addTestTab, checkTreeState, checkSidebarState, checkAuditState, selectRow, 6 toggleRow, toggleMenuItem, addA11yPanelTestsTask, navigate, 7 openSimulationMenu, toggleSimulationOption, TREE_FILTERS_MENU_ID, 8 PREFS_MENU_ID */ 9 10 "use strict"; 11 12 // Import framework's shared head. 13 Services.scriptloader.loadSubScript( 14 "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", 15 this 16 ); 17 18 // Import inspector's shared head. 19 Services.scriptloader.loadSubScript( 20 "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js", 21 this 22 ); 23 24 const { 25 ORDERED_PROPS, 26 PREF_KEYS, 27 } = require("resource://devtools/client/accessibility/constants.js"); 28 29 const SIMULATION_MENU_BUTTON_ID = "simulation-menu-button"; 30 const SIMULATION_MENU_ID = "simulation-menu-button-menu"; 31 const TREE_FILTERS_MENU_ID = "accessibility-tree-filters-menu"; 32 const PREFS_MENU_ID = "accessibility-tree-filters-prefs-menu"; 33 34 const MENU_INDEXES = { 35 [TREE_FILTERS_MENU_ID]: 0, 36 [PREFS_MENU_ID]: 1, 37 }; 38 39 /** 40 * Wait for accessibility service to shut down. We consider it shut down when 41 * an "a11y-init-or-shutdown" event is received with a value of "0". 42 */ 43 function waitForAccessibilityShutdown() { 44 return new Promise(resolve => { 45 if (!Services.appinfo.accessibilityEnabled) { 46 resolve(); 47 return; 48 } 49 50 const observe = (subject, topic, data) => { 51 if (data === "0") { 52 Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); 53 // Sanity check 54 ok( 55 !Services.appinfo.accessibilityEnabled, 56 "Accessibility disabled in this process" 57 ); 58 resolve(); 59 } 60 }; 61 // This event is coming from Gecko accessibility module when the 62 // accessibility service is shutdown or initialzied. We attempt to shutdown 63 // accessibility service naturally if there are no more XPCOM references to 64 // a11y related objects (after GC/CC). 65 Services.obs.addObserver(observe, "a11y-init-or-shutdown"); 66 67 // Force garbage collection. 68 SpecialPowers.gc(); 69 SpecialPowers.forceShrinkingGC(); 70 SpecialPowers.forceCC(); 71 }); 72 } 73 74 /** 75 * Ensure that accessibility is completely shutdown. 76 */ 77 async function shutdownAccessibility(browser) { 78 await waitForAccessibilityShutdown(); 79 await SpecialPowers.spawn(browser, [], waitForAccessibilityShutdown); 80 } 81 82 const EXPANDABLE_PROPS = ["actions", "states", "attributes"]; 83 84 /** 85 * Add a new test tab in the browser and load the given url. 86 * 87 * @param {string} url 88 * The url to be loaded in the new tab 89 * @param {object} options 90 * @param {boolean} options.waitUntilDocumentAccessibleInState 91 * Whether we should wait for the state to have the document accessible. 92 * Defaults to true. 93 * @return a promise that resolves to the tab object when 94 * the url is loaded 95 */ 96 async function addTestTab( 97 url, 98 { waitUntilDocumentAccessibleInState = true } = {} 99 ) { 100 info("Adding a new test tab with URL: '" + url + "'"); 101 102 const tab = await addTab(url); 103 const panel = await initAccessibilityPanel(tab); 104 const win = panel.panelWin; 105 const doc = win.document; 106 const store = win.view.store; 107 108 win.focus(); 109 110 if (waitUntilDocumentAccessibleInState) { 111 await waitUntilState( 112 store, 113 state => 114 state.accessibles.size === 1 && 115 state.details.accessible?.role === "document" 116 ); 117 } 118 119 return { 120 tab, 121 browser: tab.linkedBrowser, 122 panel, 123 win, 124 toolbox: panel._toolbox, 125 doc, 126 store, 127 }; 128 } 129 130 /** 131 * Open the Accessibility panel for the given tab. 132 * 133 * @param {Element} tab 134 * Optional tab element for which you want open the Accessibility panel. 135 * The default tab is taken from the global variable |tab|. 136 * @return a promise that is resolved once the panel is open. 137 */ 138 async function initAccessibilityPanel(tab = gBrowser.selectedTab) { 139 const toolbox = await gDevTools.showToolboxForTab(tab, { 140 toolId: "accessibility", 141 }); 142 return toolbox.getCurrentPanel(); 143 } 144 145 /** 146 * Compare text within the list of potential badges rendered for accessibility 147 * tree row when its accessible object has accessibility failures. 148 * 149 * @param {DOMNode} badges 150 * Container element that contains badge elements. 151 * @param {Array|null} expected 152 * List of expected badge labels for failing accessibility checks. 153 */ 154 function compareBadges(badges, expected = []) { 155 const badgeEls = badges ? [...badges.querySelectorAll(".badge")] : []; 156 return ( 157 badgeEls.length === expected.length && 158 badgeEls.every((badge, i) => badge.textContent === expected[i]) 159 ); 160 } 161 162 /** 163 * Find an ancestor that is scrolled for a given DOMNode. 164 * 165 * @param {DOMNode} node 166 * DOMNode that to find an ancestor for that is scrolled. 167 */ 168 function closestScrolledParent(node) { 169 if (node == null) { 170 return null; 171 } 172 173 if (node.scrollHeight > node.clientHeight) { 174 return node; 175 } 176 177 return closestScrolledParent(node.parentNode); 178 } 179 180 /** 181 * Check if a given element is visible to the user and is not scrolled off 182 * because of the overflow. 183 * 184 * @param {Element} element 185 * Element to be checked whether it is visible and is not scrolled off. 186 * 187 * @returns {boolean} 188 * True if the element is visible. 189 */ 190 function isVisible(element) { 191 const { top, bottom } = element.getBoundingClientRect(); 192 const scrolledParent = closestScrolledParent(element.parentNode); 193 const scrolledParentRect = scrolledParent 194 ? scrolledParent.getBoundingClientRect() 195 : null; 196 return ( 197 !scrolledParent || 198 (top >= scrolledParentRect.top && bottom <= scrolledParentRect.bottom) 199 ); 200 } 201 202 /** 203 * Check selected styling and visibility for a given row in the accessibility 204 * tree. 205 * 206 * @param {DOMNode} row 207 * DOMNode for a given accessibility row. 208 * @param {boolean} expected 209 * Expected selected state. 210 * 211 * @returns {boolean} 212 * True if visibility and styling matches expected selected state. 213 */ 214 function checkSelected(row, expected) { 215 if (!expected) { 216 return true; 217 } 218 219 if (row.classList.contains("selected") !== expected) { 220 return false; 221 } 222 223 return isVisible(row); 224 } 225 226 /** 227 * Check level for a given row in the accessibility tree. 228 * 229 * @param {DOMNode} row 230 * DOMNode for a given accessibility row. 231 * @param {boolean} expected 232 * Expected row level (aria-level). 233 * 234 * @returns {boolean} 235 * True if the aria-level for the row is as expected. 236 */ 237 function checkLevel(row, expected) { 238 if (!expected) { 239 return true; 240 } 241 242 return parseInt(row.getAttribute("aria-level"), 10) === expected; 243 } 244 245 /** 246 * Check the state of the accessibility tree. 247 * 248 * @param {document} doc panel documnent. 249 * @param {Array} expected an array that represents an expected row list. 250 */ 251 async function checkTreeState(doc, expected) { 252 info("Checking tree state."); 253 const hasExpectedStructure = await BrowserTestUtils.waitForCondition(() => { 254 const rows = [...doc.querySelectorAll(".treeRow")]; 255 if (rows.length !== expected.length) { 256 return false; 257 } 258 259 return rows.every((row, i) => { 260 const { role, name, badges, selected, level } = expected[i]; 261 return ( 262 row.querySelector(".treeLabelCell").textContent === role && 263 row.querySelector(".treeValueCell").textContent === name && 264 compareBadges(row.querySelector(".badges"), badges) && 265 checkSelected(row, selected) && 266 checkLevel(row, level) 267 ); 268 }); 269 }, "Wait for the right tree update."); 270 271 ok(hasExpectedStructure, "Tree structure is correct."); 272 } 273 274 /** 275 * Check if relations object matches what is expected. Note: targets are matched by their 276 * name and role. 277 * 278 * @param {object} relations Relations to test. 279 * @param {object} expected Expected relations. 280 * @return {boolean} True if relation types and their targers match what is 281 * expected. 282 */ 283 function relationsMatch(relations, expected) { 284 for (const relationType in expected) { 285 let expTargets = expected[relationType]; 286 expTargets = Array.isArray(expTargets) ? expTargets : [expTargets]; 287 288 let targets = relations ? relations[relationType] : []; 289 targets = Array.isArray(targets) ? targets : [targets]; 290 291 for (const index in expTargets) { 292 if (!targets[index]) { 293 return false; 294 } 295 if ( 296 expTargets[index].name !== targets[index].name || 297 expTargets[index].role !== targets[index].role 298 ) { 299 return false; 300 } 301 } 302 } 303 304 return true; 305 } 306 307 /** 308 * When comparing numerical values (for example contrast), we only care about the 2 309 * decimal points. 310 * 311 * @param {string} _ 312 * Key of the property that is parsed. 313 * @param {Any} value 314 * Value of the property that is parsed. 315 * @return {Any} 316 * Newly formatted value in case of the numeric value. 317 */ 318 function parseNumReplacer(_, value) { 319 if (typeof value === "number") { 320 return value.toFixed(2); 321 } 322 323 return value; 324 } 325 326 /** 327 * Check the state of the accessibility sidebar audit(checks). 328 * 329 * @param {object} store React store for the panel (includes store for 330 * the audit). 331 * @param {object} expectedState Expected state of the sidebar audit(checks). 332 */ 333 async function checkAuditState(store, expectedState) { 334 info("Checking audit state."); 335 await waitUntilState(store, ({ details }) => { 336 const { audit } = details; 337 338 for (const key in expectedState) { 339 const expected = expectedState[key]; 340 if (expected && typeof expected === "object") { 341 if ( 342 JSON.stringify(audit[key], parseNumReplacer) !== 343 JSON.stringify(expected, parseNumReplacer) 344 ) { 345 return false; 346 } 347 } else if (audit && audit[key] !== expected) { 348 return false; 349 } 350 } 351 352 ok(true, "Audit state is correct."); 353 return true; 354 }); 355 } 356 357 /** 358 * Check the state of the accessibility sidebar. 359 * 360 * @param {object} store React store for the panel (includes store for 361 * the sidebar). 362 * @param {object} expectedState Expected state of the sidebar. 363 */ 364 async function checkSidebarState(store, expectedState) { 365 info("Checking sidebar state."); 366 await waitUntilState(store, ({ details }) => { 367 for (const key of ORDERED_PROPS) { 368 const expected = expectedState[key]; 369 if (expected === undefined) { 370 continue; 371 } 372 373 if (key === "relations") { 374 if (!relationsMatch(details.relations, expected)) { 375 return false; 376 } 377 } else if (EXPANDABLE_PROPS.includes(key)) { 378 if ( 379 JSON.stringify(details.accessible[key]) !== JSON.stringify(expected) 380 ) { 381 return false; 382 } 383 } else if (details.accessible && details.accessible[key] !== expected) { 384 return false; 385 } 386 } 387 388 ok(true, "Sidebar state is correct."); 389 return true; 390 }); 391 } 392 393 /** 394 * Check the state of the accessibility related prefs. 395 * 396 * @param {Document} doc 397 * accessibility inspector panel document. 398 * @param {object} toolbarPrefValues 399 * Expected state of the panel prefs as well as the redux state that 400 * keeps track of it. Includes: 401 * - SCROLL_INTO_VIEW (devtools.accessibility.scroll-into-view) 402 * @param {object} store 403 * React store for the panel (includes store for the sidebar). 404 */ 405 async function checkToolbarPrefsState(doc, toolbarPrefValues, store) { 406 info("Checking toolbar prefs state."); 407 const [hasExpectedStructure] = await Promise.all([ 408 // Check that appropriate preferences are set as expected. 409 BrowserTestUtils.waitForCondition(() => { 410 return Object.keys(toolbarPrefValues).every( 411 name => 412 Services.prefs.getBoolPref(PREF_KEYS[name], false) === 413 toolbarPrefValues[name] 414 ); 415 }, "Wait for the right prefs state."), 416 // Check that ui state is set as expected. 417 waitUntilState(store, ({ ui }) => { 418 for (const name in toolbarPrefValues) { 419 if (ui[name] !== toolbarPrefValues[name]) { 420 return false; 421 } 422 } 423 424 ok(true, "UI pref state is correct."); 425 return true; 426 }), 427 ]); 428 ok(hasExpectedStructure, "Prefs state is correct."); 429 } 430 431 /** 432 * Check the state of the accessibility checks toolbar. 433 * 434 * @param {object} store 435 * React store for the panel (includes store for the sidebar). 436 * @param {object} activeToolbarFilters 437 * Expected active state of the filters in the toolbar. 438 */ 439 async function checkToolbarState(doc, activeToolbarFilters) { 440 info("Checking toolbar state."); 441 const hasExpectedStructure = await BrowserTestUtils.waitForCondition( 442 () => 443 [ 444 ...doc.querySelectorAll("#accessibility-tree-filters-menu .command"), 445 ].every( 446 (filter, i) => 447 (activeToolbarFilters[i] ? "true" : null) === 448 filter.getAttribute("aria-checked") 449 ), 450 "Wait for the right toolbar state." 451 ); 452 453 ok(hasExpectedStructure, "Toolbar state is correct."); 454 } 455 456 /** 457 * Check the state of the simulation button and menu components. 458 * 459 * @param {object} doc Panel document. 460 * @param {object} toolboxDoc Toolbox document. 461 * @param {object} expected Expected states of the simulation components: 462 * @param {boolean} expected.buttonActive 463 * @param {Array<number>} expected.checkedOptionIndices 464 * @param {Array<number>} expected.colorMatrix 465 */ 466 async function checkSimulationState(doc, toolboxDoc, expected) { 467 const { buttonActive, checkedOptionIndices, colorMatrix } = expected; 468 469 // Check simulation menu button state 470 await waitFor( 471 () => 472 doc 473 .getElementById(SIMULATION_MENU_BUTTON_ID) 474 .classList.contains("active") === buttonActive 475 ); 476 ok( 477 true, 478 `Simulation menu button contains ${buttonActive ? "active" : "base"} class.` 479 ); 480 481 // Check simulation menu options states, if specified 482 if (checkedOptionIndices) { 483 const simulationMenuOptions = toolboxDoc 484 .getElementById(SIMULATION_MENU_ID) 485 .querySelectorAll(".menuitem"); 486 487 simulationMenuOptions.forEach((menuListItem, index) => { 488 const isChecked = checkedOptionIndices.includes(index); 489 const button = menuListItem.firstChild; 490 491 is( 492 button.getAttribute("aria-checked"), 493 isChecked ? "true" : null, 494 `Simulation option ${index} is ${isChecked ? "" : "not "}selected.` 495 ); 496 }); 497 } 498 499 const docShellColorMatrix = await SpecialPowers.spawn( 500 gBrowser.selectedBrowser, 501 [], 502 () => content.window.docShell.getColorMatrix() 503 ); 504 Assert.deepEqual( 505 // The values we get from getColorMatrix have higher precisions than what is defined 506 // in the simulation matrix in the accessibility panel 507 docShellColorMatrix.map(v => v.toFixed(6)), 508 colorMatrix, 509 `docShell color matrix has expected value` 510 ); 511 } 512 513 /** 514 * Focus accessibility properties tree in the a11y inspector sidebar. If focused for the 515 * first time, the tree will select first rendered node as defult selection for keyboard 516 * purposes. 517 * 518 * @param {Document} doc accessibility inspector panel document. 519 */ 520 async function focusAccessibleProperties(doc) { 521 const tree = doc.querySelector(".tree"); 522 if (doc.activeElement !== tree) { 523 tree.focus(); 524 await BrowserTestUtils.waitForCondition( 525 () => tree.querySelector(".node.focused"), 526 "Tree selected." 527 ); 528 } 529 } 530 531 /** 532 * Select accessibility property in the sidebar. 533 * 534 * @param {Document} doc accessibility inspector panel document. 535 * @param {string} id id of the property to be selected. 536 * @return {DOMNode} Node that corresponds to the selected accessibility property. 537 */ 538 async function selectProperty(doc, id) { 539 const win = doc.defaultView; 540 let selected = false; 541 let node; 542 543 await focusAccessibleProperties(doc); 544 await BrowserTestUtils.waitForCondition(() => { 545 node = doc.getElementById(`${id}`); 546 if (node) { 547 if (selected) { 548 return node.firstChild.classList.contains("focused"); 549 } 550 551 AccessibilityUtils.setEnv({ 552 // Keyboard navigation is handled on the container level using arrow 553 // keys. 554 nonNegativeTabIndexRule: false, 555 }); 556 EventUtils.sendMouseEvent({ type: "click" }, node, win); 557 AccessibilityUtils.resetEnv(); 558 selected = true; 559 } else { 560 const tree = doc.querySelector(".tree"); 561 tree.scrollTop = parseFloat(win.getComputedStyle(tree).height); 562 } 563 564 return false; 565 }); 566 567 return node; 568 } 569 570 /** 571 * Select tree row. 572 * 573 * @param {document} doc panel documnent. 574 * @param {number} rowNumber number of the row/tree node to be selected. 575 */ 576 function selectRow(doc, rowNumber) { 577 info(`Selecting row ${rowNumber}.`); 578 AccessibilityUtils.setEnv({ 579 // Keyboard navigation is handled on the container level using arrow keys. 580 nonNegativeTabIndexRule: false, 581 }); 582 EventUtils.sendMouseEvent( 583 { type: "click" }, 584 doc.querySelectorAll(".treeRow")[rowNumber], 585 doc.defaultView 586 ); 587 AccessibilityUtils.resetEnv(); 588 } 589 590 /** 591 * Toggle an expandable tree row. 592 * 593 * @param {document} doc panel documnent. 594 * @param {number} rowNumber number of the row/tree node to be toggled. 595 */ 596 async function toggleRow(doc, rowNumber) { 597 const win = doc.defaultView; 598 const row = doc.querySelectorAll(".treeRow")[rowNumber]; 599 const twisty = row.querySelector(".theme-twisty"); 600 const expected = !twisty.classList.contains("open"); 601 602 info(`${expected ? "Expanding" : "Collapsing"} row ${rowNumber}.`); 603 604 AccessibilityUtils.setEnv({ 605 // We intentionally remove the twisty from the accessibility tree in the 606 // TreeView component and handle keyboard navigation using the arrow keys. 607 mustHaveAccessibleRule: false, 608 }); 609 EventUtils.sendMouseEvent({ type: "click" }, twisty, win); 610 AccessibilityUtils.resetEnv(); 611 await BrowserTestUtils.waitForCondition( 612 () => 613 !twisty.classList.contains("devtools-throbber") && 614 expected === twisty.classList.contains("open"), 615 "Twisty updated." 616 ); 617 } 618 619 /** 620 * Toggle a specific menu item based on its index in the menu. 621 * 622 * @param {document} toolboxDoc 623 * toolbox document. 624 * @param {document} doc 625 * panel document. 626 * @param {string} menuId 627 * The id of the menu (menuId passed to the MenuButton component) 628 * @param {number} menuItemIndex 629 * index of the menu item to be toggled. 630 */ 631 async function toggleMenuItem(doc, toolboxDoc, menuId, menuItemIndex) { 632 const toolboxWin = toolboxDoc.defaultView; 633 const panelWin = doc.defaultView; 634 635 const menuButton = doc.querySelectorAll(".toolbar-menu-button")[ 636 MENU_INDEXES[menuId] 637 ]; 638 ok(menuButton, "Expected menu button"); 639 640 const menuEl = toolboxDoc.getElementById(menuId); 641 const menuItem = menuEl.querySelectorAll(".command")[menuItemIndex]; 642 ok(menuItem, "Expected menu item"); 643 644 const expected = 645 menuItem.getAttribute("aria-checked") === "true" ? null : "true"; 646 647 // Make the menu visible first. 648 const onPopupShown = new Promise(r => 649 toolboxDoc.addEventListener("popupshown", r, { once: true }) 650 ); 651 EventUtils.synthesizeMouseAtCenter(menuButton, {}, panelWin); 652 await onPopupShown; 653 const boundingRect = menuItem.getBoundingClientRect(); 654 ok( 655 boundingRect.width > 0 && boundingRect.height > 0, 656 "Menu item is visible." 657 ); 658 659 EventUtils.synthesizeMouseAtCenter(menuItem, {}, toolboxWin); 660 await BrowserTestUtils.waitForCondition( 661 () => expected === menuItem.getAttribute("aria-checked"), 662 "Menu item updated." 663 ); 664 } 665 666 /** 667 * Open the simulation menu. 668 * 669 * @param {document} doc 670 * panel document. 671 * @param {document} toolboxDoc 672 * toolbox document. 673 */ 674 async function openSimulationMenu(doc, toolboxDoc) { 675 doc.getElementById(SIMULATION_MENU_BUTTON_ID).click(); 676 677 await BrowserTestUtils.waitForCondition(() => 678 toolboxDoc 679 .getElementById(SIMULATION_MENU_ID) 680 .classList.contains("tooltip-visible") 681 ); 682 } 683 684 /** 685 * Toggle an option in the the simulation menu. 686 * 687 * @param {document} toolboxDoc 688 * toolbox document. 689 * @param {number} 690 * index of the option in the menu 691 */ 692 async function toggleSimulationOption(toolboxDoc, optionIndex) { 693 const simulationMenu = toolboxDoc.getElementById(SIMULATION_MENU_ID); 694 const menuItemButton = 695 simulationMenu.querySelectorAll(".menuitem")[optionIndex].firstChild; 696 const previousAriaCheckedValue = menuItemButton.getAttribute("aria-checked"); 697 menuItemButton.click(); 698 699 // wait for the button state to be updated 700 await waitFor( 701 () => 702 menuItemButton.getAttribute("aria-checked") !== previousAriaCheckedValue 703 ); 704 705 // wait for the menu to be hidden 706 await BrowserTestUtils.waitForCondition( 707 () => !simulationMenu.classList.contains("tooltip-visible") 708 ); 709 } 710 711 async function findAccessibleFor( 712 { 713 toolbox: { target }, 714 panel: { 715 accessibilityProxy: { 716 accessibilityFront: { accessibleWalkerFront }, 717 }, 718 }, 719 }, 720 selector 721 ) { 722 const domWalker = (await target.getFront("inspector")).walker; 723 const node = await domWalker.querySelector(domWalker.rootNode, selector); 724 return accessibleWalkerFront.getAccessibleFor(node); 725 } 726 727 async function selectAccessibleForNode(env, selector) { 728 const { panel, win } = env; 729 const front = await findAccessibleFor(env, selector); 730 const { EVENTS } = win; 731 const onSelected = win.once(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED); 732 panel.selectAccessible(front); 733 await onSelected; 734 } 735 736 /** 737 * Iterate over setups/tests structure and test the state of the 738 * accessibility panel. 739 * 740 * @param {JSON} tests 741 * test data that has the format of: 742 * { 743 * desc {String} description for better logging 744 * setup {Function} An optional setup that needs to be 745 * performed before the state of the 746 * tree and the sidebar can be checked 747 * expected {JSON} An expected states for parts of 748 * accessibility panel: 749 * - tree: state of the accessibility tree widget 750 * - sidebar: state of the accessibility panel sidebar 751 * - audit: state of the audit redux state of the 752 * panel 753 * - toolbarPrefValues: state of the accessibility panel 754 * toolbar prefs and corresponding user 755 * preferences. 756 * - activeToolbarFilters: state of the accessibility panel 757 * toolbar filters. 758 * } 759 * @param {object} env 760 * contains all relevant environment objects (same structure as the 761 * return value of 'addTestTab' funciton) 762 */ 763 async function runA11yPanelTests(tests, env) { 764 for (const { desc, setup, expected } of tests) { 765 info(desc); 766 767 if (setup) { 768 await setup(env); 769 } 770 771 const { 772 tree, 773 sidebar, 774 audit, 775 toolbarPrefValues, 776 activeToolbarFilters, 777 simulation, 778 } = expected; 779 if (tree) { 780 await checkTreeState(env.doc, tree); 781 } 782 783 if (sidebar) { 784 await checkSidebarState(env.store, sidebar); 785 } 786 787 if (activeToolbarFilters) { 788 await checkToolbarState(env.doc, activeToolbarFilters); 789 } 790 791 if (toolbarPrefValues) { 792 await checkToolbarPrefsState(env.doc, toolbarPrefValues, env.store); 793 } 794 795 if (typeof audit !== "undefined") { 796 await checkAuditState(env.store, audit); 797 } 798 799 if (simulation) { 800 await checkSimulationState(env.doc, env.toolbox.doc, simulation); 801 } 802 } 803 } 804 805 /** 806 * Build a valid URL from an HTML snippet. 807 * 808 * @param {string} uri HTML snippet 809 * @param {object} options options for the test 810 * @return {string} built URL 811 */ 812 function buildURL(uri, options = {}) { 813 if (options.remoteIframe) { 814 const srcURL = new URL(`http://example.net/document-builder.sjs`); 815 srcURL.searchParams.append( 816 "html", 817 `<html> 818 <head> 819 <meta charset="utf-8"/> 820 <title>Accessibility Panel Test (OOP)</title> 821 </head> 822 <body>${uri}</body> 823 </html>` 824 ); 825 uri = `<iframe title="Accessibility Panel Test (OOP)" src="${srcURL.href}"/>`; 826 } 827 828 return `data:text/html;charset=UTF-8,${encodeURIComponent(uri)}`; 829 } 830 831 /** 832 * Add a test task based on the test structure and a test URL. 833 * 834 * @param {JSON} tests test data that has the format of: 835 * { 836 * desc {string} description for better logging 837 * setup {Function} An optional setup that needs to be 838 * performed before the state of the 839 * tree and the sidebar can be checked 840 * expected {JSON} An expected states for the tree and 841 * the sidebar 842 * } 843 * @param {string} uri test URL 844 * @param {string} msg a message that is printed for the test 845 * @param {object} options options for the test 846 */ 847 function addA11yPanelTestsTask(tests, uri, msg, options) { 848 addA11YPanelTask(msg, uri, env => runA11yPanelTests(tests, env), options); 849 } 850 851 /** 852 * Borrowed from framework's shared head. Close toolbox, completely disable 853 * accessibility and remove the tab. 854 * 855 * @param {Tab} 856 * tab The tab to close. 857 * @return {Promise} 858 * Resolves when the toolbox and tab have been destroyed and closed. 859 */ 860 async function closeTabToolboxAccessibility(tab = gBrowser.selectedTab) { 861 if (gDevTools.hasToolboxForTab(tab)) { 862 await gDevTools.closeToolboxForTab(tab); 863 } 864 865 await shutdownAccessibility(gBrowser.getBrowserForTab(tab)); 866 await removeTab(tab); 867 await new Promise(resolve => setTimeout(resolve, 0)); 868 } 869 870 /** 871 * A wrapper function around add_task that sets up the test environment, runs 872 * the test and then disables accessibility tools. 873 * 874 * @param {string} msg a message that is printed for the test 875 * @param {string} uri absolute test URL or HTML snippet 876 * @param {Function} task task function containing the tests. 877 * @param {object} options options for the test 878 */ 879 function addA11YPanelTask(msg, uri, task, options = {}) { 880 add_task(async function a11YPanelTask() { 881 info(msg); 882 883 const env = await addTestTab( 884 uri.startsWith("http") ? uri : buildURL(uri, options) 885 ); 886 await task(env); 887 await closeTabToolboxAccessibility(env.tab); 888 }); 889 }