summaryrefslogtreecommitdiffstats
path: root/devtools/client/accessibility/test/browser/head.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/accessibility/test/browser/head.js')
-rw-r--r--devtools/client/accessibility/test/browser/head.js823
1 files changed, 823 insertions, 0 deletions
diff --git a/devtools/client/accessibility/test/browser/head.js b/devtools/client/accessibility/test/browser/head.js
new file mode 100644
index 0000000000..1a94c723e0
--- /dev/null
+++ b/devtools/client/accessibility/test/browser/head.js
@@ -0,0 +1,823 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* global waitUntilState, gBrowser */
+/* exported addTestTab, checkTreeState, checkSidebarState, checkAuditState, selectRow,
+ toggleRow, toggleMenuItem, addA11yPanelTestsTask, navigate,
+ openSimulationMenu, toggleSimulationOption, TREE_FILTERS_MENU_ID,
+ PREFS_MENU_ID */
+
+"use strict";
+
+// Import framework's shared head.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js",
+ this
+);
+
+// Import inspector's shared head.
+Services.scriptloader.loadSubScript(
+ "chrome://mochitests/content/browser/devtools/client/inspector/test/shared-head.js",
+ this
+);
+
+const {
+ ORDERED_PROPS,
+ PREF_KEYS,
+} = require("resource://devtools/client/accessibility/constants.js");
+
+// Enable the Accessibility panel
+Services.prefs.setBoolPref("devtools.accessibility.enabled", true);
+
+const SIMULATION_MENU_BUTTON_ID = "#simulation-menu-button";
+const TREE_FILTERS_MENU_ID = "accessibility-tree-filters-menu";
+const PREFS_MENU_ID = "accessibility-tree-filters-prefs-menu";
+
+const MENU_INDEXES = {
+ [TREE_FILTERS_MENU_ID]: 0,
+ [PREFS_MENU_ID]: 1,
+};
+
+/**
+ * Wait for accessibility service to shut down. We consider it shut down when
+ * an "a11y-init-or-shutdown" event is received with a value of "0".
+ */
+function waitForAccessibilityShutdown() {
+ return new Promise(resolve => {
+ if (!Services.appinfo.accessibilityEnabled) {
+ resolve();
+ return;
+ }
+
+ const observe = (subject, topic, data) => {
+ if (data === "0") {
+ Services.obs.removeObserver(observe, "a11y-init-or-shutdown");
+ // Sanity check
+ ok(
+ !Services.appinfo.accessibilityEnabled,
+ "Accessibility disabled in this process"
+ );
+ resolve();
+ }
+ };
+ // This event is coming from Gecko accessibility module when the
+ // accessibility service is shutdown or initialzied. We attempt to shutdown
+ // accessibility service naturally if there are no more XPCOM references to
+ // a11y related objects (after GC/CC).
+ Services.obs.addObserver(observe, "a11y-init-or-shutdown");
+
+ // Force garbage collection.
+ SpecialPowers.gc();
+ SpecialPowers.forceShrinkingGC();
+ SpecialPowers.forceCC();
+ });
+}
+
+/**
+ * Ensure that accessibility is completely shutdown.
+ */
+async function shutdownAccessibility(browser) {
+ await waitForAccessibilityShutdown();
+ await SpecialPowers.spawn(browser, [], waitForAccessibilityShutdown);
+}
+
+registerCleanupFunction(async () => {
+ info("Cleaning up...");
+ Services.prefs.clearUserPref("devtools.accessibility.enabled");
+});
+
+const EXPANDABLE_PROPS = ["actions", "states", "attributes"];
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url
+ * The url to be loaded in the new tab
+ * @return a promise that resolves to the tab object when
+ * the url is loaded
+ */
+async function addTestTab(url) {
+ info("Adding a new test tab with URL: '" + url + "'");
+
+ const tab = await addTab(url);
+ const panel = await initAccessibilityPanel(tab);
+ const win = panel.panelWin;
+ const doc = win.document;
+ const store = win.view.store;
+
+ const enableButton = doc.getElementById("accessibility-enable-button");
+ // If enable button is not found, asume the tool is already enabled.
+ if (enableButton) {
+ EventUtils.sendMouseEvent({ type: "click" }, enableButton, win);
+ }
+
+ await waitUntilState(
+ store,
+ state =>
+ state.accessibles.size === 1 &&
+ state.details.accessible &&
+ state.details.accessible.role === "document"
+ );
+
+ return {
+ tab,
+ browser: tab.linkedBrowser,
+ panel,
+ win,
+ toolbox: panel._toolbox,
+ doc,
+ store,
+ };
+}
+
+/**
+ * Open the Accessibility panel for the given tab.
+ *
+ * @param {Element} tab
+ * Optional tab element for which you want open the Accessibility panel.
+ * The default tab is taken from the global variable |tab|.
+ * @return a promise that is resolved once the panel is open.
+ */
+async function initAccessibilityPanel(tab = gBrowser.selectedTab) {
+ const toolbox = await gDevTools.showToolboxForTab(tab, {
+ toolId: "accessibility",
+ });
+ return toolbox.getCurrentPanel();
+}
+
+/**
+ * Compare text within the list of potential badges rendered for accessibility
+ * tree row when its accessible object has accessibility failures.
+ * @param {DOMNode} badges
+ * Container element that contains badge elements.
+ * @param {Array|null} expected
+ * List of expected badge labels for failing accessibility checks.
+ */
+function compareBadges(badges, expected = []) {
+ const badgeEls = badges ? [...badges.querySelectorAll(".badge")] : [];
+ return (
+ badgeEls.length === expected.length &&
+ badgeEls.every((badge, i) => badge.textContent === expected[i])
+ );
+}
+
+/**
+ * Find an ancestor that is scrolled for a given DOMNode.
+ *
+ * @param {DOMNode} node
+ * DOMNode that to find an ancestor for that is scrolled.
+ */
+function closestScrolledParent(node) {
+ if (node == null) {
+ return null;
+ }
+
+ if (node.scrollHeight > node.clientHeight) {
+ return node;
+ }
+
+ return closestScrolledParent(node.parentNode);
+}
+
+/**
+ * Check if a given element is visible to the user and is not scrolled off
+ * because of the overflow.
+ *
+ * @param {Element} element
+ * Element to be checked whether it is visible and is not scrolled off.
+ *
+ * @returns {Boolean}
+ * True if the element is visible.
+ */
+function isVisible(element) {
+ const { top, bottom } = element.getBoundingClientRect();
+ const scrolledParent = closestScrolledParent(element.parentNode);
+ const scrolledParentRect = scrolledParent
+ ? scrolledParent.getBoundingClientRect()
+ : null;
+ return (
+ !scrolledParent ||
+ (top >= scrolledParentRect.top && bottom <= scrolledParentRect.bottom)
+ );
+}
+
+/**
+ * Check selected styling and visibility for a given row in the accessibility
+ * tree.
+ * @param {DOMNode} row
+ * DOMNode for a given accessibility row.
+ * @param {Boolean} expected
+ * Expected selected state.
+ *
+ * @returns {Boolean}
+ * True if visibility and styling matches expected selected state.
+ */
+function checkSelected(row, expected) {
+ if (!expected) {
+ return true;
+ }
+
+ if (row.classList.contains("selected") !== expected) {
+ return false;
+ }
+
+ return isVisible(row);
+}
+
+/**
+ * Check level for a given row in the accessibility tree.
+ * @param {DOMNode} row
+ * DOMNode for a given accessibility row.
+ * @param {Boolean} expected
+ * Expected row level (aria-level).
+ *
+ * @returns {Boolean}
+ * True if the aria-level for the row is as expected.
+ */
+function checkLevel(row, expected) {
+ if (!expected) {
+ return true;
+ }
+
+ return parseInt(row.getAttribute("aria-level"), 10) === expected;
+}
+
+/**
+ * Check the state of the accessibility tree.
+ * @param {document} doc panel documnent.
+ * @param {Array} expected an array that represents an expected row list.
+ */
+async function checkTreeState(doc, expected) {
+ info("Checking tree state.");
+ const hasExpectedStructure = await BrowserTestUtils.waitForCondition(() => {
+ const rows = [...doc.querySelectorAll(".treeRow")];
+ if (rows.length !== expected.length) {
+ return false;
+ }
+
+ return rows.every((row, i) => {
+ const { role, name, badges, selected, level } = expected[i];
+ return (
+ row.querySelector(".treeLabelCell").textContent === role &&
+ row.querySelector(".treeValueCell").textContent === name &&
+ compareBadges(row.querySelector(".badges"), badges) &&
+ checkSelected(row, selected) &&
+ checkLevel(row, level)
+ );
+ });
+ }, "Wait for the right tree update.");
+
+ ok(hasExpectedStructure, "Tree structure is correct.");
+}
+
+/**
+ * Check if relations object matches what is expected. Note: targets are matched by their
+ * name and role.
+ * @param {Object} relations Relations to test.
+ * @param {Object} expected Expected relations.
+ * @return {Boolean} True if relation types and their targers match what is
+ * expected.
+ */
+function relationsMatch(relations, expected) {
+ for (const relationType in expected) {
+ let expTargets = expected[relationType];
+ expTargets = Array.isArray(expTargets) ? expTargets : [expTargets];
+
+ let targets = relations ? relations[relationType] : [];
+ targets = Array.isArray(targets) ? targets : [targets];
+
+ for (const index in expTargets) {
+ if (!targets[index]) {
+ return false;
+ }
+ if (
+ expTargets[index].name !== targets[index].name ||
+ expTargets[index].role !== targets[index].role
+ ) {
+ return false;
+ }
+ }
+ }
+
+ return true;
+}
+
+/**
+ * When comparing numerical values (for example contrast), we only care about the 2
+ * decimal points.
+ * @param {String} _
+ * Key of the property that is parsed.
+ * @param {Any} value
+ * Value of the property that is parsed.
+ * @return {Any}
+ * Newly formatted value in case of the numeric value.
+ */
+function parseNumReplacer(_, value) {
+ if (typeof value === "number") {
+ return value.toFixed(2);
+ }
+
+ return value;
+}
+
+/**
+ * Check the state of the accessibility sidebar audit(checks).
+ * @param {Object} store React store for the panel (includes store for
+ * the audit).
+ * @param {Object} expectedState Expected state of the sidebar audit(checks).
+ */
+async function checkAuditState(store, expectedState) {
+ info("Checking audit state.");
+ await waitUntilState(store, ({ details }) => {
+ const { audit } = details;
+
+ for (const key in expectedState) {
+ const expected = expectedState[key];
+ if (expected && typeof expected === "object") {
+ if (
+ JSON.stringify(audit[key], parseNumReplacer) !==
+ JSON.stringify(expected, parseNumReplacer)
+ ) {
+ return false;
+ }
+ } else if (audit && audit[key] !== expected) {
+ return false;
+ }
+ }
+
+ ok(true, "Audit state is correct.");
+ return true;
+ });
+}
+
+/**
+ * Check the state of the accessibility sidebar.
+ * @param {Object} store React store for the panel (includes store for
+ * the sidebar).
+ * @param {Object} expectedState Expected state of the sidebar.
+ */
+async function checkSidebarState(store, expectedState) {
+ info("Checking sidebar state.");
+ await waitUntilState(store, ({ details }) => {
+ for (const key of ORDERED_PROPS) {
+ const expected = expectedState[key];
+ if (expected === undefined) {
+ continue;
+ }
+
+ if (key === "relations") {
+ if (!relationsMatch(details.relations, expected)) {
+ return false;
+ }
+ } else if (EXPANDABLE_PROPS.includes(key)) {
+ if (
+ JSON.stringify(details.accessible[key]) !== JSON.stringify(expected)
+ ) {
+ return false;
+ }
+ } else if (details.accessible && details.accessible[key] !== expected) {
+ return false;
+ }
+ }
+
+ ok(true, "Sidebar state is correct.");
+ return true;
+ });
+}
+
+/**
+ * Check the state of the accessibility related prefs.
+ * @param {Document} doc
+ * accessibility inspector panel document.
+ * @param {Object} toolbarPrefValues
+ * Expected state of the panel prefs as well as the redux state that
+ * keeps track of it. Includes:
+ * - SCROLL_INTO_VIEW (devtools.accessibility.scroll-into-view)
+ * @param {Object} store
+ * React store for the panel (includes store for the sidebar).
+ */
+async function checkToolbarPrefsState(doc, toolbarPrefValues, store) {
+ info("Checking toolbar prefs state.");
+ const [hasExpectedStructure] = await Promise.all([
+ // Check that appropriate preferences are set as expected.
+ BrowserTestUtils.waitForCondition(() => {
+ return Object.keys(toolbarPrefValues).every(
+ name =>
+ Services.prefs.getBoolPref(PREF_KEYS[name], false) ===
+ toolbarPrefValues[name]
+ );
+ }, "Wait for the right prefs state."),
+ // Check that ui state is set as expected.
+ waitUntilState(store, ({ ui }) => {
+ for (const name in toolbarPrefValues) {
+ if (ui[name] !== toolbarPrefValues[name]) {
+ return false;
+ }
+ }
+
+ ok(true, "UI pref state is correct.");
+ return true;
+ }),
+ ]);
+ ok(hasExpectedStructure, "Prefs state is correct.");
+}
+
+/**
+ * Check the state of the accessibility checks toolbar.
+ * @param {Object} store
+ * React store for the panel (includes store for the sidebar).
+ * @param {Object} activeToolbarFilters
+ * Expected active state of the filters in the toolbar.
+ */
+async function checkToolbarState(doc, activeToolbarFilters) {
+ info("Checking toolbar state.");
+ const hasExpectedStructure = await BrowserTestUtils.waitForCondition(
+ () =>
+ [
+ ...doc.querySelectorAll("#accessibility-tree-filters-menu .command"),
+ ].every(
+ (filter, i) =>
+ (activeToolbarFilters[i] ? "true" : null) ===
+ filter.getAttribute("aria-checked")
+ ),
+ "Wait for the right toolbar state."
+ );
+
+ ok(hasExpectedStructure, "Toolbar state is correct.");
+}
+
+/**
+ * Check the state of the simulation button and menu components.
+ * @param {Object} doc Panel document.
+ * @param {Object} expected Expected states of the simulation components:
+ * menuVisible, buttonActive, checkedOptionIndices (Optional)
+ */
+async function checkSimulationState(doc, expected) {
+ const { buttonActive, checkedOptionIndices } = expected;
+ const simulationMenuOptions = doc
+ .querySelector(SIMULATION_MENU_BUTTON_ID + "-menu")
+ .querySelectorAll(".menuitem");
+
+ // Check simulation menu button state
+ is(
+ doc.querySelector(SIMULATION_MENU_BUTTON_ID).className,
+ `devtools-button toolbar-menu-button simulation${
+ buttonActive ? " active" : ""
+ }`,
+ `Simulation menu button contains ${buttonActive ? "active" : "base"} class.`
+ );
+
+ // Check simulation menu options states, if specified
+ if (checkedOptionIndices) {
+ simulationMenuOptions.forEach((menuListItem, index) => {
+ const isChecked = checkedOptionIndices.includes(index);
+ const button = menuListItem.firstChild;
+
+ is(
+ button.getAttribute("aria-checked"),
+ isChecked ? "true" : null,
+ `Simulation option ${index} is ${isChecked ? "" : "not "}selected.`
+ );
+ });
+ }
+}
+
+/**
+ * Focus accessibility properties tree in the a11y inspector sidebar. If focused for the
+ * first time, the tree will select first rendered node as defult selection for keyboard
+ * purposes.
+ *
+ * @param {Document} doc accessibility inspector panel document.
+ */
+async function focusAccessibleProperties(doc) {
+ const tree = doc.querySelector(".tree");
+ if (doc.activeElement !== tree) {
+ tree.focus();
+ await BrowserTestUtils.waitForCondition(
+ () => tree.querySelector(".node.focused"),
+ "Tree selected."
+ );
+ }
+}
+
+/**
+ * Select accessibility property in the sidebar.
+ * @param {Document} doc accessibility inspector panel document.
+ * @param {String} id id of the property to be selected.
+ * @return {DOMNode} Node that corresponds to the selected accessibility property.
+ */
+async function selectProperty(doc, id) {
+ const win = doc.defaultView;
+ let selected = false;
+ let node;
+
+ await focusAccessibleProperties(doc);
+ await BrowserTestUtils.waitForCondition(() => {
+ node = doc.getElementById(`${id}`);
+ if (node) {
+ if (selected) {
+ return node.firstChild.classList.contains("focused");
+ }
+
+ AccessibilityUtils.setEnv({
+ // Keyboard navigation is handled on the container level using arrow
+ // keys.
+ nonNegativeTabIndexRule: false,
+ });
+ EventUtils.sendMouseEvent({ type: "click" }, node, win);
+ AccessibilityUtils.resetEnv();
+ selected = true;
+ } else {
+ const tree = doc.querySelector(".tree");
+ tree.scrollTop = parseFloat(win.getComputedStyle(tree).height);
+ }
+
+ return false;
+ });
+
+ return node;
+}
+
+/**
+ * Select tree row.
+ * @param {document} doc panel documnent.
+ * @param {Number} rowNumber number of the row/tree node to be selected.
+ */
+function selectRow(doc, rowNumber) {
+ info(`Selecting row ${rowNumber}.`);
+ AccessibilityUtils.setEnv({
+ // Keyboard navigation is handled on the container level using arrow keys.
+ nonNegativeTabIndexRule: false,
+ });
+ EventUtils.sendMouseEvent(
+ { type: "click" },
+ doc.querySelectorAll(".treeRow")[rowNumber],
+ doc.defaultView
+ );
+ AccessibilityUtils.resetEnv();
+}
+
+/**
+ * Toggle an expandable tree row.
+ * @param {document} doc panel documnent.
+ * @param {Number} rowNumber number of the row/tree node to be toggled.
+ */
+async function toggleRow(doc, rowNumber) {
+ const win = doc.defaultView;
+ const row = doc.querySelectorAll(".treeRow")[rowNumber];
+ const twisty = row.querySelector(".theme-twisty");
+ const expected = !twisty.classList.contains("open");
+
+ info(`${expected ? "Expanding" : "Collapsing"} row ${rowNumber}.`);
+
+ AccessibilityUtils.setEnv({
+ // We intentionally remove the twisty from the accessibility tree in the
+ // TreeView component and handle keyboard navigation using the arrow keys.
+ mustHaveAccessibleRule: false,
+ });
+ EventUtils.sendMouseEvent({ type: "click" }, twisty, win);
+ AccessibilityUtils.resetEnv();
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ !twisty.classList.contains("devtools-throbber") &&
+ expected === twisty.classList.contains("open"),
+ "Twisty updated."
+ );
+}
+
+/**
+ * Toggle a specific menu item based on its index in the menu.
+ * @param {document} toolboxDoc
+ * toolbox document.
+ * @param {document} doc
+ * panel document.
+ * @param {String} menuId
+ * The id of the menu (menuId passed to the MenuButton component)
+ * @param {Number} menuItemIndex
+ * index of the menu item to be toggled.
+ */
+async function toggleMenuItem(doc, toolboxDoc, menuId, menuItemIndex) {
+ const toolboxWin = toolboxDoc.defaultView;
+ const panelWin = doc.defaultView;
+
+ const menuButton = doc.querySelectorAll(".toolbar-menu-button")[
+ MENU_INDEXES[menuId]
+ ];
+ ok(menuButton, "Expected menu button");
+
+ const menuEl = toolboxDoc.getElementById(menuId);
+ const menuItem = menuEl.querySelectorAll(".command")[menuItemIndex];
+ ok(menuItem, "Expected menu item");
+
+ const expected =
+ menuItem.getAttribute("aria-checked") === "true" ? null : "true";
+
+ // Make the menu visible first.
+ const onPopupShown = new Promise(r =>
+ toolboxDoc.addEventListener("popupshown", r, { once: true })
+ );
+ EventUtils.synthesizeMouseAtCenter(menuButton, {}, panelWin);
+ await onPopupShown;
+ const boundingRect = menuItem.getBoundingClientRect();
+ ok(
+ boundingRect.width > 0 && boundingRect.height > 0,
+ "Menu item is visible."
+ );
+
+ EventUtils.synthesizeMouseAtCenter(menuItem, {}, toolboxWin);
+ await BrowserTestUtils.waitForCondition(
+ () => expected === menuItem.getAttribute("aria-checked"),
+ "Menu item updated."
+ );
+}
+
+async function openSimulationMenu(doc) {
+ doc.querySelector(SIMULATION_MENU_BUTTON_ID).click();
+
+ await BrowserTestUtils.waitForCondition(() =>
+ doc
+ .querySelector(SIMULATION_MENU_BUTTON_ID + "-menu")
+ .classList.contains("tooltip-visible")
+ );
+}
+
+async function toggleSimulationOption(doc, optionIndex) {
+ const simulationMenu = doc.querySelector(SIMULATION_MENU_BUTTON_ID + "-menu");
+ simulationMenu.querySelectorAll(".menuitem")[optionIndex].firstChild.click();
+
+ await BrowserTestUtils.waitForCondition(
+ () => !simulationMenu.classList.contains("tooltip-visible")
+ );
+}
+
+async function findAccessibleFor(
+ {
+ toolbox: { target },
+ panel: {
+ accessibilityProxy: {
+ accessibilityFront: { accessibleWalkerFront },
+ },
+ },
+ },
+ selector
+) {
+ const domWalker = (await target.getFront("inspector")).walker;
+ const node = await domWalker.querySelector(domWalker.rootNode, selector);
+ return accessibleWalkerFront.getAccessibleFor(node);
+}
+
+async function selectAccessibleForNode(env, selector) {
+ const { panel, win } = env;
+ const front = await findAccessibleFor(env, selector);
+ const { EVENTS } = win;
+ const onSelected = win.once(EVENTS.NEW_ACCESSIBLE_FRONT_SELECTED);
+ panel.selectAccessible(front);
+ await onSelected;
+}
+
+/**
+ * Iterate over setups/tests structure and test the state of the
+ * accessibility panel.
+ * @param {JSON} tests
+ * test data that has the format of:
+ * {
+ * desc {String} description for better logging
+ * setup {Function} An optional setup that needs to be
+ * performed before the state of the
+ * tree and the sidebar can be checked
+ * expected {JSON} An expected states for parts of
+ * accessibility panel:
+ * - tree: state of the accessibility tree widget
+ * - sidebar: state of the accessibility panel sidebar
+ * - audit: state of the audit redux state of the
+ * panel
+ * - toolbarPrefValues: state of the accessibility panel
+ * toolbar prefs and corresponding user
+ * preferences.
+ * - activeToolbarFilters: state of the accessibility panel
+ * toolbar filters.
+ * }
+ * @param {Object} env
+ * contains all relevant environment objects (same structure as the
+ * return value of 'addTestTab' funciton)
+ */
+async function runA11yPanelTests(tests, env) {
+ for (const { desc, setup, expected } of tests) {
+ info(desc);
+
+ if (setup) {
+ await setup(env);
+ }
+
+ const {
+ tree,
+ sidebar,
+ audit,
+ toolbarPrefValues,
+ activeToolbarFilters,
+ simulation,
+ } = expected;
+ if (tree) {
+ await checkTreeState(env.doc, tree);
+ }
+
+ if (sidebar) {
+ await checkSidebarState(env.store, sidebar);
+ }
+
+ if (activeToolbarFilters) {
+ await checkToolbarState(env.doc, activeToolbarFilters);
+ }
+
+ if (toolbarPrefValues) {
+ await checkToolbarPrefsState(env.doc, toolbarPrefValues, env.store);
+ }
+
+ if (typeof audit !== "undefined") {
+ await checkAuditState(env.store, audit);
+ }
+
+ if (simulation) {
+ await checkSimulationState(env.doc, simulation);
+ }
+ }
+}
+
+/**
+ * Build a valid URL from an HTML snippet.
+ * @param {String} uri HTML snippet
+ * @param {Object} options options for the test
+ * @return {String} built URL
+ */
+function buildURL(uri, options = {}) {
+ if (options.remoteIframe) {
+ const srcURL = new URL(`http://example.net/document-builder.sjs`);
+ srcURL.searchParams.append(
+ "html",
+ `<html>
+ <head>
+ <meta charset="utf-8"/>
+ <title>Accessibility Panel Test (OOP)</title>
+ </head>
+ <body>${uri}</body>
+ </html>`
+ );
+ uri = `<iframe title="Accessibility Panel Test (OOP)" src="${srcURL.href}"/>`;
+ }
+
+ return `data:text/html;charset=UTF-8,${encodeURIComponent(uri)}`;
+}
+
+/**
+ * Add a test task based on the test structure and a test URL.
+ * @param {JSON} tests test data that has the format of:
+ * {
+ * desc {String} description for better logging
+ * setup {Function} An optional setup that needs to be
+ * performed before the state of the
+ * tree and the sidebar can be checked
+ * expected {JSON} An expected states for the tree and
+ * the sidebar
+ * }
+ * @param {String} uri test URL
+ * @param {String} msg a message that is printed for the test
+ * @param {Object} options options for the test
+ */
+function addA11yPanelTestsTask(tests, uri, msg, options) {
+ addA11YPanelTask(msg, uri, env => runA11yPanelTests(tests, env), options);
+}
+
+/**
+ * Borrowed from framework's shared head. Close toolbox, completely disable
+ * accessibility and remove the tab.
+ * @param {Tab}
+ * tab The tab to close.
+ * @return {Promise}
+ * Resolves when the toolbox and tab have been destroyed and closed.
+ */
+async function closeTabToolboxAccessibility(tab = gBrowser.selectedTab) {
+ if (gDevTools.hasToolboxForTab(tab)) {
+ await gDevTools.closeToolboxForTab(tab);
+ }
+
+ await shutdownAccessibility(gBrowser.getBrowserForTab(tab));
+ await removeTab(tab);
+ await new Promise(resolve => setTimeout(resolve, 0));
+}
+
+/**
+ * A wrapper function around add_task that sets up the test environment, runs
+ * the test and then disables accessibility tools.
+ * @param {String} msg a message that is printed for the test
+ * @param {String} uri test URL
+ * @param {Function} task task function containing the tests.
+ * @param {Object} options options for the test
+ */
+function addA11YPanelTask(msg, uri, task, options = {}) {
+ add_task(async function a11YPanelTask() {
+ info(msg);
+
+ const env = await addTestTab(buildURL(uri, options));
+ await task(env);
+ await closeTabToolboxAccessibility(env.tab);
+ });
+}