diff options
Diffstat (limited to 'devtools/client/accessibility/test/browser')
28 files changed, 3969 insertions, 0 deletions
diff --git a/devtools/client/accessibility/test/browser/browser.ini b/devtools/client/accessibility/test/browser/browser.ini new file mode 100644 index 0000000000..3058fac933 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser.ini @@ -0,0 +1,49 @@ +[DEFAULT] +tags = devtools +subsuite = devtools +skip-if = (os == 'win' && processor == 'aarch64') +support-files = + head.js + !/devtools/client/shared/test/shared-head.js + !/devtools/client/shared/test/highlighter-test-actor.js + !/devtools/client/inspector/test/shared-head.js + !/devtools/client/shared/test/telemetry-test-helpers.js + +[browser_accessibility_context_menu_browser.js] +skip-if = (os == 'win' && processor == 'aarch64') # bug 1533184 +[browser_accessibility_context_menu_inspector.js] +skip-if = + (os == 'win' && processor == 'aarch64') # bug 1533484 + win10_2004 # Bug 1723573 + apple_catalina # Bug 1713392 +[browser_accessibility_fission_switch_target.js] +https_first_disabled = true +skip-if = (os == 'linux' && asan) # bug 1666940 +[browser_accessibility_mutations.js] +skip-if = + os == 'win' && processor == 'aarch64' # bug 1533534 + os == 'linux' && bits == 64 && fission # Bug 1675445 +[browser_accessibility_panel_audit_hidden_iframe.js] +[browser_accessibility_panel_audit_oop.js] +[browser_accessibility_panel_toolbar_checks.js] +[browser_accessibility_panel_toolbar_pref_scroll.js] +skip-if = true # bug 1674060 +[browser_accessibility_print_to_json.js] +[browser_accessibility_relation_navigation.js] +[browser_accessibility_reload.js] +[browser_accessibility_sidebar_checks.js] +[browser_accessibility_sidebar_dom_nodes.js] +[browser_accessibility_sidebar.js] +[browser_accessibility_simulation.js] +skip-if = true # bug 1674060 +[browser_accessibility_tabbing_order_highlighter_iframe_picker.js] +[browser_accessibility_tabbing_order_highlighter.js] +[browser_accessibility_tree_audit_long.js] +[browser_accessibility_tree_audit_reset.js] +[browser_accessibility_tree_audit_toolbar.js] +[browser_accessibility_tree_audit.js] +[browser_accessibility_tree_contrast.js] +[browser_accessibility_tree_iframe_picker.js] +[browser_accessibility_tree_navigation_oop.js] +[browser_accessibility_tree_navigation.js] +[browser_accessibility_tree.js] diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_context_menu_browser.js b/devtools/client/accessibility/test/browser/browser_accessibility_context_menu_browser.js new file mode 100644 index 0000000000..0491b29eb4 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_context_menu_browser.js @@ -0,0 +1,74 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = '<h1 id="h1">header</h1><p id="p">paragraph</p>'; + +addA11YPanelTask( + "Test show accessibility properties context menu in browser.", + TEST_URI, + async function ({ panel, toolbox, browser }) { + // Load the inspector to ensure it to use in this test. + await toolbox.loadTool("inspector"); + + const headerSelector = "#h1"; + + const contextMenu = document.getElementById("contentAreaContextMenu"); + const awaitPopupShown = BrowserTestUtils.waitForEvent( + contextMenu, + "popupshown" + ); + await BrowserTestUtils.synthesizeMouse( + headerSelector, + 0, + 0, + { + type: "contextmenu", + button: 2, + centered: true, + }, + browser + ); + await awaitPopupShown; + + const inspectA11YPropsItem = contextMenu.querySelector( + "#context-inspect-a11y" + ); + + info( + "Triggering 'Inspect Accessibility Properties' and waiting for " + + "accessibility panel to open" + ); + const popupHidden = BrowserTestUtils.waitForEvent( + contextMenu, + "popuphidden" + ); + contextMenu.activateItem(inspectA11YPropsItem); + await popupHidden; + + const selected = await panel.once("new-accessible-front-selected"); + const expectedSelectedNode = await getNodeFront( + headerSelector, + toolbox.getPanel("inspector") + ); + const expectedSelected = + await panel.accessibilityProxy.accessibilityFront.accessibleWalkerFront.getAccessibleFor( + expectedSelectedNode + ); + is( + toolbox.getCurrentPanel(), + panel, + "Accessibility panel is currently selected" + ); + is(selected, expectedSelected, "Accessible front selected correctly"); + + const doc = panel.panelWin.document; + const propertiesTree = doc.querySelector(".tree"); + is(doc.activeElement, propertiesTree, "Properties list must be focused."); + ok( + isVisible(doc.querySelector(".treeTable .treeRow.selected")), + "Selected row is visible." + ); + } +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_context_menu_inspector.js b/devtools/client/accessibility/test/browser/browser_accessibility_context_menu_inspector.js new file mode 100644 index 0000000000..03273a30c2 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_context_menu_inspector.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. +http://creativecommons.org/publicdomain/zero/1.0/ */ +"use strict"; + +const TEST_URI = ` + <h1 id="h1">header</h1> + <p id="p">paragraph</p> + <span id="span-1">text</span> + <span id="span-2"> + IamaverylongtextwhichdoesntfitinInlineTextChildReallyIdontIamtoobig + </span>`; + +async function openContextMenuForNode({ toolbox }, selector) { + info("Selecting Inspector tab and opening a context menu"); + const inspector = await toolbox.selectTool("inspector"); + + if (!selector) { + ok(inspector.selection.isBodyNode(), "Default selection is a body node."); + } else if (typeof selector === "string") { + await selectNode(selector, inspector, "test"); + } else { + const updated = inspector.once("inspector-updated"); + inspector.selection.setNodeFront(selector, { reason: "test" }); + await updated; + } + + return openContextMenuAndGetAllItems(inspector); +} + +function checkShowA11YPropertiesNode(allMenuItems) { + const showA11YPropertiesNode = allMenuItems.find( + item => item.id === "node-menu-showaccessibilityproperties" + ); + ok( + showA11YPropertiesNode, + "the popup menu now has a show accessibility properties item" + ); + return showA11YPropertiesNode; +} + +async function checkAccessibleObjectSelection( + { toolbox, panel }, + menuItem, + isText +) { + const inspector = await toolbox.getPanel("inspector"); + info( + "Triggering 'Show Accessibility Properties' and waiting for " + + "accessibility panel to open" + ); + const panelSelected = toolbox.once("accessibility-selected"); + const objectSelected = panel.once("new-accessible-front-selected"); + menuItem.click(); + await panelSelected; + const selected = await objectSelected; + + const expectedNode = isText + ? inspector.selection.nodeFront.inlineTextChild + : inspector.selection.nodeFront; + const expectedSelected = + await panel.accessibilityProxy.accessibilityFront.accessibleWalkerFront.getAccessibleFor( + expectedNode + ); + is(selected, expectedSelected, "Accessible front selected correctly"); + + const doc = panel.panelWin.document; + const propertiesTree = doc.querySelector(".tree"); + is(doc.activeElement, propertiesTree, "Properties list must be focused."); + ok( + isVisible(doc.querySelector(".treeTable .treeRow.selected")), + "Selected row is visible." + ); +} + +addA11YPanelTask( + "Test show accessibility properties context menu.", + TEST_URI, + async function testShowAccessibilityPropertiesContextMenu(env) { + // Load the inspector to ensure it to use in this test. + await env.toolbox.loadTool("inspector"); + + let allMenuItems = await openContextMenuForNode(env); + let showA11YPropertiesNode = checkShowA11YPropertiesNode(allMenuItems); + + allMenuItems = await openContextMenuForNode(env, "#h1"); + showA11YPropertiesNode = checkShowA11YPropertiesNode(allMenuItems); + await checkAccessibleObjectSelection(env, showA11YPropertiesNode); + + allMenuItems = await openContextMenuForNode(env, "#span-1"); + showA11YPropertiesNode = checkShowA11YPropertiesNode(allMenuItems); + await checkAccessibleObjectSelection(env, showA11YPropertiesNode, true); + + allMenuItems = await openContextMenuForNode(env, "#span-2"); + showA11YPropertiesNode = checkShowA11YPropertiesNode(allMenuItems); + + const inspector = env.toolbox.getPanel("inspector"); + const span2 = await getNodeFront("#span-2", inspector); + await inspector.markup.expandNode(span2); + const { nodes } = await inspector.walker.children(span2); + allMenuItems = await openContextMenuForNode(env, nodes[0]); + showA11YPropertiesNode = checkShowA11YPropertiesNode(allMenuItems); + await checkAccessibleObjectSelection(env, showA11YPropertiesNode, false); + } +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_fission_switch_target.js b/devtools/client/accessibility/test/browser/browser_accessibility_fission_switch_target.js new file mode 100644 index 0000000000..9da56a0b1b --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_fission_switch_target.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test switching for the top-level target. + +const MAIN_PROCESS_URL = "about:robots"; +const MAIN_PROCESS_EXPECTED = [ + { + expected: { + sidebar: { + name: "Gort! Klaatu barada nikto!", + role: "document", + }, + }, + }, +]; + +const CONTENT_PROCESS_URL = buildURL(`<title>Test page</title>`); +const CONTENT_PROCESS_EXPECTED = [ + { + expected: { + sidebar: { + name: "Test page", + role: "document", + relations: { + "containing document": { + role: "document", + name: "Test page", + }, + embeds: { + role: "document", + name: "Test page", + }, + }, + }, + }, + }, +]; + +add_task(async () => { + info( + "Open a test page running on the content process and accessibility panel" + ); + const env = await addTestTab(CONTENT_PROCESS_URL); + await runA11yPanelTests(CONTENT_PROCESS_EXPECTED, env); + + info("Navigate to a page running on the main process"); + await navigateTo(MAIN_PROCESS_URL); + await runA11yPanelTests(MAIN_PROCESS_EXPECTED, env); + + info("Back to a page running on the content process"); + await navigateTo(CONTENT_PROCESS_URL); + await runA11yPanelTests(CONTENT_PROCESS_EXPECTED, env); + + await closeTabToolboxAccessibility(env.tab); +}); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_mutations.js b/devtools/client/accessibility/test/browser/browser_accessibility_mutations.js new file mode 100644 index 0000000000..4c564747f9 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_mutations.js @@ -0,0 +1,217 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1 id="h1">Top level header</h1> + <p id="p">This is a paragraph.</p> + </body> +</html>`; + +const documentRow = { + role: "document", + name: `"Accessibility Panel Test"`, +}; +const documentRowOOP = { + role: "document", + name: `""text label`, + badges: ["text label"], +}; +const subtree = [ + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "text leaf", + name: `"Top level header"`, + }, + { + role: "paragraph", + name: `""`, + }, +]; +const frameSubtree = [ + { role: "internal frame", name: `"Accessibility Panel Test (OOP)"` }, + { + role: "document", + name: `"Accessibility Panel Test (OOP)"`, + }, +]; +const subtreeOOP = [...frameSubtree, ...subtree]; +const renamed = [ + { + role: "heading", + name: `"New Header"`, + }, + { + role: "text leaf", + name: `"New Header"`, + }, +]; +const paragraphSidebar = { + name: null, + role: "paragraph", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 1, + indexInParent: 1, + states: ["selectable text", "opaque", "enabled", "sensitive"], +}; +const headerSidebar = { + name: "Top level header", + role: "text leaf", +}; +const newHeaderSidebar = { + name: "New Header", +}; + +function removeRow(rowNumber) { + return async ({ doc, browser }) => { + is( + doc.querySelectorAll(".treeRow").length, + rowNumber, + "Tree size is correct." + ); + await SpecialPowers.spawn(browser, [], async () => { + const iframe = content.document.getElementsByTagName("iframe")[0]; + if (iframe) { + await SpecialPowers.spawn(iframe, [], () => + content.document.getElementById("p").remove() + ); + return; + } + + content.document.getElementById("p").remove(); + }); + await BrowserTestUtils.waitForCondition( + () => doc.querySelectorAll(".treeRow").length === rowNumber - 1, + "Tree updated." + ); + }; +} + +async function rename({ browser }) { + await SpecialPowers.spawn(browser, [], async () => { + const iframe = content.document.getElementsByTagName("iframe")[0]; + if (iframe) { + await SpecialPowers.spawn( + iframe, + [], + () => (content.document.getElementById("h1").textContent = "New Header") + ); + return; + } + + content.document.getElementById("h1").textContent = "New Header"; + }); +} + +/** + * Test data 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. + * } + */ +const testsTopLevel = [ + { + desc: "Expand first and second rows, select third row.", + setup: async ({ doc }) => { + await toggleRow(doc, 0); + await toggleRow(doc, 1); + selectRow(doc, 3); + }, + expected: { + tree: [documentRow, ...subtree], + sidebar: paragraphSidebar, + }, + }, + { + desc: "Remove a child from a document.", + setup: removeRow(4), + expected: { + tree: [documentRow, ...subtree.slice(0, -1)], + sidebar: headerSidebar, + }, + }, + { + desc: "Update child's text content.", + setup: rename, + expected: { + tree: [documentRow, ...renamed], + }, + }, + { + desc: "Select third row in the tree.", + setup: ({ doc }) => selectRow(doc, 1), + expected: { + sidebar: newHeaderSidebar, + }, + }, +]; + +const testsOOP = [ + { + desc: "Expand rows until we reach an internal OOP frame.", + setup: async ({ doc }) => { + await toggleRow(doc, 0); + await toggleRow(doc, 1); + await toggleRow(doc, 2); + await toggleRow(doc, 3); + selectRow(doc, 5); + }, + expected: { + tree: [documentRowOOP, ...subtreeOOP], + sidebar: paragraphSidebar, + }, + }, + { + desc: "Remove a child from a document.", + setup: removeRow(6), + expected: { + tree: [documentRowOOP, ...subtreeOOP.slice(0, -1)], + sidebar: headerSidebar, + }, + }, + { + desc: "Update child's text content.", + setup: rename, + expected: { + tree: [documentRowOOP, ...frameSubtree, ...renamed], + }, + }, + { + desc: "Select third row in the tree.", + setup: ({ doc }) => selectRow(doc, 1), + expected: { + sidebar: newHeaderSidebar, + }, + }, +]; + +/** + * Tests that checks the Accessibility panel after DOM tree mutations. + */ +addA11yPanelTestsTask( + testsTopLevel, + TEST_URI, + "Test Accessibility panel after DOM tree mutations." +); + +addA11yPanelTestsTask( + testsOOP, + TEST_URI, + "Test Accessibility panel after DOM tree mutations in the OOP frame.", + { remoteIframe: true } +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_panel_audit_hidden_iframe.js b/devtools/client/accessibility/test/browser/browser_accessibility_panel_audit_hidden_iframe.js new file mode 100644 index 0000000000..1e7d0e26c2 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_panel_audit_hidden_iframe.js @@ -0,0 +1,66 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global toggleMenuItem, TREE_FILTERS_MENU_ID */ + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1 style="color:rgba(255,0,0,0.1); background-color:rgba(255,255,255,1);"> + Top level header + </h1> + <iframe style="display: none"></iframe> + <iframe style="display: none" src="data:text/html,iframe"></iframe> + <iframe style="display: none" src="https://example.com/document-builder.sjs?html=oop"></iframe> + </body> + </html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Initial state.", + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + ], + }, + }, + { + desc: "Click on the Check for issues - all.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: [ + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + level: 1, + }, + ], + }, + }, +]; + +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree audit on a page with hidden iframes." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_panel_audit_oop.js b/devtools/client/accessibility/test/browser/browser_accessibility_panel_audit_oop.js new file mode 100644 index 0000000000..d1ccbe9d5c --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_panel_audit_oop.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global toggleMenuItem, TREE_FILTERS_MENU_ID */ + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1 style="color:rgba(255,0,0,0.1); background-color:rgba(255,255,255,1);"> + Top level header + </h1> + </body> + </html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Initial state.", + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + level: 1, + }, + ], + }, + }, + { + desc: "Click on the Check for issues - all.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + level: 1, + }, + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + level: 1, + }, + ], + }, + }, + { + desc: "Click on the Check for issues - all again.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + level: 1, + }, + { + role: "internal frame", + name: `"Accessibility Panel Test (OOP)"`, + level: 2, + }, + { + role: "document", + name: `"Accessibility Panel Test (OOP)"`, + level: 3, + }, + { + role: "heading", + name: `"Top level header"`, + level: 4, + }, + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + level: 5, + }, + ], + }, + }, +]; + +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree audit on a page with an OOP document.", + { remoteIframe: true } +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_panel_toolbar_checks.js b/devtools/client/accessibility/test/browser/browser_accessibility_panel_toolbar_checks.js new file mode 100644 index 0000000000..b7d541853b --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_panel_toolbar_checks.js @@ -0,0 +1,114 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global toggleMenuItem, TREE_FILTERS_MENU_ID */ + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body></body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Check initial state.", + expected: { + activeToolbarFilters: [true, false, false, false, false], + }, + }, + { + desc: "Toggle first filter (all) to activate.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + activeToolbarFilters: [false, true, true, true, true], + }, + }, + { + desc: "Click on the filter again.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + activeToolbarFilters: [true, false, false, false, false], + }, + }, + { + desc: "Toggle first custom filter to activate.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 2); + }, + expected: { + activeToolbarFilters: [false, false, true, false, false], + }, + }, + { + desc: "Click on the filter again.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 2); + }, + expected: { + activeToolbarFilters: [true, false, false, false, false], + }, + }, + { + desc: "Toggle first custom filter to activate.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 2); + }, + expected: { + activeToolbarFilters: [false, false, true, false, false], + }, + }, + { + desc: "Toggle second custom filter to activate.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 3); + }, + expected: { + activeToolbarFilters: [false, false, true, true, false], + }, + }, + { + desc: "Toggle third custom filter to activate.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 4); + }, + expected: { + activeToolbarFilters: [false, true, true, true, true], + }, + }, + { + desc: "Click on the none filter to de-activate all.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 0); + }, + expected: { + activeToolbarFilters: [true, false, false, false, false], + }, + }, +]; + +/** + * Simple test that checks toggle states for filters in the Accessibility panel + * toolbar. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel filter toggle states." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_panel_toolbar_pref_scroll.js b/devtools/client/accessibility/test/browser/browser_accessibility_panel_toolbar_pref_scroll.js new file mode 100644 index 0000000000..9f8434f695 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_panel_toolbar_pref_scroll.js @@ -0,0 +1,73 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global toggleMenuItem, PREFS_MENU_ID */ + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body></body> +</html>`; + +const { + PREFS: { SCROLL_INTO_VIEW }, +} = require("resource://devtools/client/accessibility/constants.js"); + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Check initial state. All filters are disabled (except none). Scroll into view pref disabled.", + expected: { + activeToolbarFilters: [true, false, false, false], + toolbarPrefValues: { + [SCROLL_INTO_VIEW]: false, + }, + }, + }, + { + desc: "Toggle scroll into view checkbox to set the pref. Scroll into view pref should be enabled.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, PREFS_MENU_ID, 0); + }, + expected: { + activeToolbarFilters: [true, false, false, false], + toolbarPrefValues: { + [SCROLL_INTO_VIEW]: true, + }, + }, + }, + { + desc: "Toggle off scroll into view checkbox to unset the pref. Scroll into view pref disabled.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, PREFS_MENU_ID, 0); + }, + expected: { + activeToolbarFilters: [true, false, false, false], + toolbarPrefValues: { + [SCROLL_INTO_VIEW]: false, + }, + }, + }, +]; + +/** + * Simple test that checks toggle state and pref set for automatic scroll into + * view setting. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel scroll into view pref." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_print_to_json.js b/devtools/client/accessibility/test/browser/browser_accessibility_print_to_json.js new file mode 100644 index 0000000000..b7641430c0 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_print_to_json.js @@ -0,0 +1,245 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = "<h1>Top level header</h1>"; + +function getMenuItems(toolbox) { + const menuDoc = toolbox.doc.defaultView.windowRoot.ownerGlobal.document; + const menu = menuDoc.getElementById("accessibility-row-contextmenu"); + return { + menu, + items: [...menu.getElementsByTagName("menuitem")], + }; +} + +async function newTabSelected(tab) { + info("Waiting for the JSON viewer tab."); + await BrowserTestUtils.waitForCondition( + () => gBrowser.selectedTab !== tab, + "Current tab updated." + ); + return gBrowser.selectedTab; +} + +function parseSnapshotFromTabURI(tab) { + let snapshot = tab.label.split("data:application/json;charset=UTF-8,")[1]; + snapshot = decodeURIComponent(snapshot); + return JSON.parse(snapshot); +} + +async function checkJSONSnapshotForRow({ doc, tab, toolbox }, index, expected) { + info(`Triggering context menu for row #${index}.`); + EventUtils.synthesizeMouseAtCenter( + doc.querySelectorAll(".treeRow")[index], + { type: "contextmenu" }, + doc.defaultView + ); + + info(`Triggering "Print To JSON" menu item for row ${index}.`); + const { + menu, + items: [printToJSON], + } = getMenuItems(toolbox); + + await BrowserTestUtils.waitForPopupEvent(menu, "shown"); + + menu.activateItem(printToJSON); + + const jsonViewTab = await newTabSelected(tab); + Assert.deepEqual( + parseSnapshotFromTabURI(jsonViewTab), + expected, + "JSON snapshot for the whole document is correct" + ); + + await removeTab(jsonViewTab); +} + +const OOP_FRAME_DOCUMENT_SNAPSHOT = { + childCount: 1, + description: "", + indexInParent: 0, + keyboardShortcut: "", + name: "Accessibility Panel Test (OOP)", + nodeCssSelector: "", + nodeType: 9, + role: "document", + value: "", + actions: [], + attributes: { + display: "block", + "explicit-name": "true", + "margin-bottom": "8px", + "margin-left": "8px", + "margin-right": "8px", + "margin-top": "8px", + tag: "body", + "text-align": "start", + "text-indent": "0px", + }, + states: ["readonly", "focusable", "opaque", "enabled", "sensitive"], + children: [ + { + childCount: 1, + description: "", + indexInParent: 0, + keyboardShortcut: "", + name: "Top level header", + nodeCssSelector: "body > h1:nth-child(1)", + nodeType: 1, + role: "heading", + value: "", + actions: [], + attributes: { + display: "block", + formatting: "block", + level: "1", + "margin-bottom": "21.44px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "0px", + tag: "h1", + "text-align": "start", + "text-indent": "0px", + }, + states: ["selectable text", "opaque", "enabled", "sensitive"], + children: [ + { + childCount: 0, + description: "", + indexInParent: 0, + keyboardShortcut: "", + name: "Top level header", + nodeCssSelector: "body > h1:nth-child(1)#text", + nodeType: 3, + role: "text leaf", + value: "", + actions: [], + attributes: { + "explicit-name": "true", + }, + states: ["opaque", "enabled", "sensitive"], + children: [], + }, + ], + }, + ], +}; + +const OOP_FRAME_SNAPSHOT = { + childCount: 1, + description: "", + indexInParent: 0, + keyboardShortcut: "", + name: "Accessibility Panel Test (OOP)", + nodeCssSelector: "body > iframe:nth-child(1)", + nodeType: 1, + role: "internal frame", + value: "", + actions: [], + attributes: { + display: "inline", + "explicit-name": "true", + "margin-bottom": "0px", + "margin-left": "0px", + "margin-right": "0px", + "margin-top": "0px", + tag: "iframe", + "text-align": "start", + "text-indent": "0px", + }, + states: ["focusable", "opaque", "enabled", "sensitive"], + children: [OOP_FRAME_DOCUMENT_SNAPSHOT], +}; + +const EXPECTED_SNAPSHOT = { + childCount: 1, + description: "", + indexInParent: 0, + keyboardShortcut: "", + name: "", + nodeCssSelector: "", + nodeType: 9, + role: "document", + value: "", + actions: [], + attributes: { + display: "block", + "explicit-name": "true", + "margin-bottom": "8px", + "margin-left": "8px", + "margin-right": "8px", + "margin-top": "8px", + tag: "body", + "text-align": "start", + "text-indent": "0px", + }, + states: ["readonly", "focusable", "opaque", "enabled", "sensitive"], + children: [OOP_FRAME_SNAPSHOT], +}; + +addA11YPanelTask( + "Test print to JSON functionality.", + TEST_URI, + async env => { + const { doc } = env; + await runA11yPanelTests( + [ + { + desc: "Test the initial accessibility tree.", + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + }, + ], + }, + }, + ], + env + ); + + await toggleRow(doc, 0); + await toggleRow(doc, 1); + + await runA11yPanelTests( + [ + { + desc: "Test expanded accessibility tree.", + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + }, + { + role: "internal frame", + name: `"Accessibility Panel Test (OOP)"`, + }, + { + role: "document", + name: `"Accessibility Panel Test (OOP)"`, + }, + ], + }, + }, + ], + env + ); + + // Complete snapshot that includes OOP frame document (crossing process boundry). + await checkJSONSnapshotForRow(env, 0, EXPECTED_SNAPSHOT); + // Snapshot of an OOP frame (crossing process boundry). + await checkJSONSnapshotForRow(env, 1, OOP_FRAME_SNAPSHOT); + // Snapshot of an OOP frame document (not crossing process boundry). + await checkJSONSnapshotForRow(env, 2, OOP_FRAME_DOCUMENT_SNAPSHOT); + }, + { + remoteIframe: true, + } +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_relation_navigation.js b/devtools/client/accessibility/test/browser/browser_accessibility_relation_navigation.js new file mode 100644 index 0000000000..5f9b5e6eb5 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_relation_navigation.js @@ -0,0 +1,169 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + L10N, +} = require("resource://devtools/client/accessibility/utils/l10n.js"); + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1>Top level header</h1> + <p>This is a paragraph.</p> + </body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Test the initial accessibility tree and sidebar states.", + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + ], + sidebar: { + name: "Accessibility Panel Test", + role: "document", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 2, + indexInParent: 0, + states: [ + // The focused state is an outdated state, since the toolbox should now + // have the focus and not the content page. See Bug 1702709. + "focused", + "readonly", + "focusable", + "opaque", + "enabled", + "sensitive", + ], + }, + }, + }, + { + desc: "Expand first tree node.", + setup: ({ doc }) => toggleRow(doc, 0), + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "paragraph", + name: `""`, + }, + ], + }, + }, + { + desc: "Select second tree node.", + setup: ({ doc }) => selectRow(doc, 1), + expected: { + sidebar: { + name: "Top level header", + role: "heading", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 1, + indexInParent: 0, + relations: { + "containing document": { + role: "document", + name: "Accessibility Panel Test", + }, + }, + states: ["selectable text", "opaque", "enabled", "sensitive"], + }, + }, + }, + { + desc: "Select containing document.", + setup: async ({ doc, win }) => { + const relations = await selectProperty(doc, "/relations"); + AccessibilityUtils.setEnv({ + // Keyboard navigation is handled on the container level using arrow + // keys. + mustHaveAccessibleRule: false, + }); + EventUtils.sendMouseEvent( + { type: "click" }, + relations.querySelector(".arrow"), + win + ); + AccessibilityUtils.resetEnv(); + const containingDocRelation = await selectProperty( + doc, + "/relations/containing document" + ); + AccessibilityUtils.setEnv({ + // Keyboard interaction is only enabled when the row is selected and + // activated. + nonNegativeTabIndexRule: false, + }); + + const selectElementInTreeButton = containingDocRelation.querySelector( + ".open-accessibility-inspector" + ); + ok(!!selectElementInTreeButton, "There's a button to select the element"); + is( + selectElementInTreeButton.getAttribute("title"), + L10N.getStr("accessibility.accessible.selectElement.title"), + "The button has the expected title" + ); + EventUtils.sendMouseEvent( + { type: "click" }, + selectElementInTreeButton, + win + ); + AccessibilityUtils.resetEnv(); + }, + expected: { + sidebar: { + name: "Accessibility Panel Test", + role: "document", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 2, + indexInParent: 0, + states: ["readonly", "focusable", "opaque", "enabled", "sensitive"], + }, + }, + }, +]; + +/** + * Check navigation within the tree. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel relation navigation." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_reload.js b/devtools/client/accessibility/test/browser/browser_accessibility_reload.js new file mode 100644 index 0000000000..c4ffdf30dd --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_reload.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI_1 = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1>Top level header</h1> + <p>This is a paragraph.</p> + </body> +</html>`; + +const TEST_URI_2 = `<html> + <head> + <meta charset="utf-8"/> + <title>Navigation Accessibility Panel</title> + </head> + <body></body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Test the initial accessibility tree state after first row is expanded.", + setup: async ({ doc }) => toggleRow(doc, 0), + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "paragraph", + name: `""`, + }, + ], + sidebar: { + name: "Accessibility Panel Test", + role: "document", + }, + }, + }, + { + desc: "Reload the page.", + setup: async ({ panel }) => { + const onReloaded = panel.once("reloaded"); + panel.accessibilityProxy.commands.targetCommand.reloadTopLevelTarget(); + await onReloaded; + }, + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + ], + sidebar: { + name: "Accessibility Panel Test", + role: "document", + }, + }, + }, + { + desc: "Navigate to a new page.", + setup: async () => { + // `navigate` waits for the "reloaded" event so we don't need to do it explicitly here + await navigateTo(buildURL(TEST_URI_2)); + }, + expected: { + tree: [ + { + role: "document", + name: `"Navigation Accessibility Panel"`, + }, + ], + sidebar: { + name: "Navigation Accessibility Panel", + role: "document", + }, + }, + }, +]; + +/** + * Simple test that checks content of the Accessibility panel tree on reload. + */ +addA11yPanelTestsTask( + tests, + TEST_URI_1, + "Test Accessibility panel tree on reload." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_sidebar.js b/devtools/client/accessibility/test/browser/browser_accessibility_sidebar.js new file mode 100644 index 0000000000..e7c2795ced --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_sidebar.js @@ -0,0 +1,81 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body></body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Test the initial accessibility sidebar state.", + expected: { + sidebar: { + name: "Accessibility Panel Test", + role: "document", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 0, + indexInParent: 0, + states: [ + // The focused state is an outdated state, since the toolbox should now + // have the focus and not the content page. See Bug 1702709. + "focused", + "readonly", + "focusable", + "opaque", + "enabled", + "sensitive", + ], + }, + }, + }, + { + desc: "Mark document as disabled for accessibility.", + setup: async ({ browser }) => + SpecialPowers.spawn(browser, [], () => + content.document.body.setAttribute("aria-disabled", true) + ), + expected: { + sidebar: { + states: ["unavailable", "readonly", "focusable", "opaque"], + }, + }, + }, + { + desc: "Append a new child to the document.", + setup: async ({ browser }) => + SpecialPowers.spawn(browser, [], () => { + const doc = content.document; + const button = doc.createElement("button"); + button.textContent = "Press Me!"; + doc.body.appendChild(button); + }), + expected: { + sidebar: { + childCount: 1, + }, + }, + }, +]; + +/** + * Test that checks the Accessibility panel sidebar. + */ +addA11yPanelTestsTask(tests, TEST_URI, "Test Accessibility panel sidebar."); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_sidebar_checks.js b/devtools/client/accessibility/test/browser/browser_accessibility_sidebar_checks.js new file mode 100644 index 0000000000..304d562189 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_sidebar_checks.js @@ -0,0 +1,80 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + accessibility: { SCORES }, +} = require("resource://devtools/shared/constants.js"); + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <p style="color: red;">Red</p> + <p style="color: blue;">Blue</p> + <p style="color: gray; background: linear-gradient(#e66465, #9198e5);">Gray</p> + </body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Test the initial accessibility audit state.", + expected: { + audit: { CONTRAST: null }, + }, + }, + { + desc: "Check accessible representing text node in red.", + setup: async ({ doc }) => { + await toggleRow(doc, 0); + await toggleRow(doc, 1); + await selectRow(doc, 2); + }, + expected: { + audit: { + CONTRAST: { + value: 4.0, + color: [255, 0, 0, 1], + backgroundColor: [255, 255, 255, 1], + isLargeText: false, + score: SCORES.FAIL, + }, + }, + }, + }, + { + desc: "Check accessible representing text node in blue.", + setup: async ({ doc }) => { + await toggleRow(doc, 3); + await selectRow(doc, 4); + }, + expected: { + audit: { + CONTRAST: { + value: 8.59, + color: [0, 0, 255, 1], + backgroundColor: [255, 255, 255, 1], + isLargeText: false, + score: SCORES.AAA, + }, + }, + }, + }, +]; + +/** + * Test that checks the Accessibility panel sidebar. + */ +addA11yPanelTestsTask(tests, TEST_URI, "Test Accessibility panel sidebar."); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_sidebar_dom_nodes.js b/devtools/client/accessibility/test/browser/browser_accessibility_sidebar_dom_nodes.js new file mode 100644 index 0000000000..075354b0c3 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_sidebar_dom_nodes.js @@ -0,0 +1,111 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const { + L10N, +} = require("resource://devtools/client/accessibility/utils/l10n.js"); + +// Check that DOM nodes in the sidebar can be highlighted and that clicking on the icon +// next to them opens the inspector. + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Sidebar DOM Nodes Test</title> + </head> + <body> + <h1 id="select-me">Hello</h1> + </body> +</html>`; + +/** + * Test that checks the Accessibility panel sidebar. + */ +addA11YPanelTask( + "Check behavior of DOM nodes in side panel", + TEST_URI, + async ({ toolbox, doc }) => { + info("Select an item having an actual associated DOM node"); + await toggleRow(doc, 0); + selectRow(doc, 1); + + await BrowserTestUtils.waitForCondition( + () => getPropertyValue(doc, "name") === `"Hello"`, + "Wait until the sidebar is updated" + ); + + info("Check DOMNode"); + const domNodeEl = getPropertyItem(doc, "DOMNode"); + ok(domNodeEl, "The DOMNode item was retrieved"); + + const openInspectorButton = domNodeEl.querySelector(".open-inspector"); + ok(openInspectorButton, "The open inspector button is displayed"); + is( + openInspectorButton.getAttribute("title"), + L10N.getStr("accessibility.accessible.selectNodeInInspector.title"), + "The open inspector button has expected title" + ); + + info("Check that hovering DOMNode triggers the highlight"); + // Loading the inspector panel at first, to make it possible to listen for + // new node selections + await toolbox.loadTool("inspector"); + const highlighter = toolbox.getHighlighter(); + const highlighterTestFront = await getHighlighterTestFront(toolbox); + + const onHighlighterShown = highlighter.waitForHighlighterShown(); + + EventUtils.synthesizeMouseAtCenter( + openInspectorButton, + { type: "mousemove" }, + doc.defaultView + ); + + const { nodeFront } = await onHighlighterShown; + is(nodeFront.displayName, "h1", "The correct node was highlighted"); + isVisible = await highlighterTestFront.isHighlighting(); + ok(isVisible, "Highlighter is displayed"); + + info("Unhighlight the node by moving away from the node"); + const onHighlighterHidden = highlighter.waitForHighlighterHidden(); + EventUtils.synthesizeMouseAtCenter( + getPropertyItem(doc, "name"), + { type: "mousemove" }, + doc.defaultView + ); + + await onHighlighterHidden; + ok(true, "The highlighter was closed when moving away from the node"); + + info( + "Clicking on the inspector icon and waiting for the inspector to be selected" + ); + const onNewNode = toolbox.selection.once("new-node-front"); + openInspectorButton.click(); + const inspectorSelectedNodeFront = await onNewNode; + + ok(true, "Inspector selected and new node got selected"); + is( + inspectorSelectedNodeFront.id, + "select-me", + "The expected node was selected" + ); + } +); + +function getPropertyItem(doc, label) { + const labelEl = Array.from( + doc.querySelectorAll("#accessibility-properties .object-label") + ).find(el => el.textContent === label); + if (!labelEl) { + return null; + } + return labelEl.closest(".node"); +} + +function getPropertyValue(doc, label) { + return getPropertyItem(doc, label)?.querySelector(".object-value") + ?.textContent; +} diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_simulation.js b/devtools/client/accessibility/test/browser/browser_accessibility_simulation.js new file mode 100644 index 0000000000..85a9be6b36 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_simulation.js @@ -0,0 +1,99 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global openSimulationMenu, toggleSimulationOption */ + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Simulations Test</title> + </head> + <body> + <h1 style="color:blue; background-color:rgba(255,255,255,1);"> + Top level header + </h1> + <h2 style="color:green; background-color:rgba(255,255,255,1);"> + Second level header + </h2> + </body> +</html>`; + +/** + * Test data 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 simulation components can be checked. + * expected {JSON} An expected states for the simulation components. + * } + */ +const tests = [ + { + desc: "Check that the menu button is inactivate and the menu is closed initially.", + expected: { + simulation: { + buttonActive: false, + }, + }, + }, + { + desc: "Clicking the menu button shows the menu with No Simulation selected.", + setup: async ({ doc }) => { + await openSimulationMenu(doc); + }, + expected: { + simulation: { + buttonActive: false, + checkedOptionIndices: [0], + }, + }, + }, + { + desc: "Selecting an option renders the menu button active and closes the menu.", + setup: async ({ doc }) => { + await toggleSimulationOption(doc, 2); + }, + expected: { + simulation: { + buttonActive: true, + checkedOptionIndices: [2], + }, + }, + }, + { + desc: "Reopening the menu preserves the previously selected option.", + setup: async ({ doc }) => { + await openSimulationMenu(doc); + }, + expected: { + simulation: { + buttonActive: true, + checkedOptionIndices: [2], + }, + }, + }, + { + desc: "Unselecting the option renders the button inactive and closes the menu.", + setup: async ({ doc }) => { + await toggleSimulationOption(doc, 2); + }, + expected: { + simulation: { + buttonActive: false, + checkedOptionIndices: [0], + }, + }, + }, +]; + +/** + * Test that checks state of simulation button and menu when + * options are selected/unselected with web render enabled. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test selecting and unselecting simulation options updates UI." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tabbing_order_highlighter.js b/devtools/client/accessibility/test/browser/browser_accessibility_tabbing_order_highlighter.js new file mode 100644 index 0000000000..ed872f46e0 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tabbing_order_highlighter.js @@ -0,0 +1,140 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check "Show tabbing order" works as expected + +const TEST_URI = `https://example.com/document-builder.sjs?html= + <button id=top-btn-1>Top level button before iframe</button> + <iframe src="https://example.org/document-builder.sjs?html=${encodeURIComponent(` + <button id=iframe-org-btn-1>in iframe button 1</button> + <button id=iframe-org-btn-2>in iframe button 2</button> + `)}"></iframe> + <button id=top-btn-2>Top level button after iframe</button> + <iframe src="https://example.net/document-builder.sjs?html=${encodeURIComponent(` + <button id=iframe-net-btn-1>in iframe button 1</button> + `)}"></iframe>`; + +add_task(async () => { + const { doc, store, tab, toolbox } = await addTestTab(TEST_URI); + + const topLevelFrameHighlighterTestFront = await toolbox.target.getFront( + "highlighterTest" + ); + + const frameTargets = toolbox.commands.targetCommand.getAllTargets([ + toolbox.commands.targetCommand.TYPES.FRAME, + ]); + const orgIframeTarget = frameTargets.find(t => + t.url.startsWith("https://example.org") + ); + const netIframeTarget = frameTargets.find(t => + t.url.startsWith("https://example.net") + ); + + // The iframe only has a dedicated target when Fission or EFT is enabled + const orgIframeHighlighterTestFront = orgIframeTarget + ? await orgIframeTarget.getFront("highlighterTest") + : null; + const netIframeHighlighterTestFront = netIframeTarget + ? await netIframeTarget.getFront("highlighterTest") + : null; + + let tabbingOrderHighlighterData = + await topLevelFrameHighlighterTestFront.getTabbingOrderHighlighterData(); + is( + tabbingOrderHighlighterData.length, + 0, + "Tabbing order is not visible at first" + ); + + info(`Click on "Show Tabbing Order" checkbox`); + const tabbingOrderCheckbox = doc.getElementById( + "devtools-display-tabbing-order-checkbox" + ); + tabbingOrderCheckbox.click(); + + await waitUntilState(store, state => state.ui.tabbingOrderDisplayed === true); + + is(tabbingOrderCheckbox.checked, true, "Checkbox is checked"); + tabbingOrderHighlighterData = + await topLevelFrameHighlighterTestFront.getTabbingOrderHighlighterData(); + if (isFissionEnabled()) { + // ⚠️ We don't get the highlighter for the <html> node of the iframe when Fission is enabled. + // This should be fix as part of Bug 1740509. + is( + JSON.stringify(tabbingOrderHighlighterData), + JSON.stringify([`button#top-btn-1 : 1`, `button#top-btn-2 : 4`]), + "Tabbing order is visible for the top level target after clicking the checkbox" + ); + + const orgIframeTabingOrderHighlighterData = + await orgIframeHighlighterTestFront.getTabbingOrderHighlighterData(); + is( + JSON.stringify(orgIframeTabingOrderHighlighterData), + JSON.stringify([ + `button#iframe-org-btn-1 : 2`, + `button#iframe-org-btn-2 : 3`, + ]), + "Tabbing order is visible for the org iframe after clicking the checkbox" + ); + + const netIframeTabingOrderHighlighterData = + await netIframeHighlighterTestFront.getTabbingOrderHighlighterData(); + is( + JSON.stringify(netIframeTabingOrderHighlighterData), + JSON.stringify([`button#iframe-net-btn-1 : 5`]), + "Tabbing order is visible for the net iframe after clicking the checkbox" + ); + } else { + is( + JSON.stringify(tabbingOrderHighlighterData), + JSON.stringify([ + `button#top-btn-1 : 1`, + `html : 2`, + `button#iframe-org-btn-1 : 3`, + `button#iframe-org-btn-2 : 4`, + `button#top-btn-2 : 5`, + `html : 6`, + `button#iframe-net-btn-1 : 7`, + ]), + "Tabbing order is visible for the top level target after clicking the checkbox" + ); + } + + info(`Clicking on the checkbox again hides the highlighter`); + tabbingOrderCheckbox.click(); + await waitUntilState( + store, + state => state.ui.tabbingOrderDisplayed === false + ); + + is(tabbingOrderCheckbox.checked, false, "Checkbox is unchecked"); + tabbingOrderHighlighterData = + await topLevelFrameHighlighterTestFront.getTabbingOrderHighlighterData(); + is( + tabbingOrderHighlighterData.length, + 0, + "Tabbing order is not visible anymore after unchecking the checkbox" + ); + + if (isFissionEnabled()) { + const orgIframeTabingOrderHighlighterData = + await orgIframeHighlighterTestFront.getTabbingOrderHighlighterData(); + is( + orgIframeTabingOrderHighlighterData.length, + 0, + "Tabbing order is also hidden on the org iframe target" + ); + const netIframeTabingOrderHighlighterData = + await netIframeHighlighterTestFront.getTabbingOrderHighlighterData(); + is( + netIframeTabingOrderHighlighterData.length, + 0, + "Tabbing order is also hidden on the net iframe target" + ); + } + + await closeTabToolboxAccessibility(tab); +}); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tabbing_order_highlighter_iframe_picker.js b/devtools/client/accessibility/test/browser/browser_accessibility_tabbing_order_highlighter_iframe_picker.js new file mode 100644 index 0000000000..788813d343 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tabbing_order_highlighter_iframe_picker.js @@ -0,0 +1,195 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check "Show tabbing order" works as expected when used with the iframe picker + +const TEST_URI = `https://example.com/document-builder.sjs?html= + <button id=top-btn-1>Top level button before iframe</button> + <iframe src="https://example.org/document-builder.sjs?html=${encodeURIComponent(` + <button id=iframe-btn-1>in iframe button 1</button> + <button id=iframe-btn-2>in iframe button 2</button> + `)}"></iframe> + <button id=top-btn-2>Top level button after iframe</button>`; + +add_task(async () => { + const env = await addTestTab(TEST_URI); + const { doc, panel, store, toolbox, win } = env; + + const topLevelFrameHighlighterTestFront = await toolbox.target.getFront( + "highlighterTest" + ); + + const iframeTarget = toolbox.commands.targetCommand + .getAllTargets([toolbox.commands.targetCommand.TYPES.FRAME]) + .find(t => t.url.startsWith("https://example.org")); + + // The iframe only has a dedicated target when Fission or EFT is enabled + const iframeHighlighterTestFront = iframeTarget + ? await iframeTarget.getFront("highlighterTest") + : null; + + const topLevelAccessibilityFrontActorID = + panel.accessibilityProxy.accessibilityFront.actorID; + + const iframeAccessibilityFrontActorID = iframeTarget + ? (await iframeTarget.getFront("accessibility")).actorID + : null; + + info(`Click on "Show Tabbing Order" checkbox`); + const tabbingOrderCheckbox = doc.getElementById( + "devtools-display-tabbing-order-checkbox" + ); + tabbingOrderCheckbox.click(); + await waitUntilState(store, state => state.ui.tabbingOrderDisplayed === true); + + is(tabbingOrderCheckbox.checked, true, "Checkbox is checked"); + let tabbingOrderHighlighterData = + await topLevelFrameHighlighterTestFront.getTabbingOrderHighlighterData( + topLevelAccessibilityFrontActorID + ); + if (isFissionEnabled()) { + // ⚠️ We don't get the highlighter for the <html> node of the iframe when Fission is enabled. + // This should be fix as part of Bug 1740509. + is( + JSON.stringify(tabbingOrderHighlighterData), + JSON.stringify([`button#top-btn-1 : 1`, `button#top-btn-2 : 4`]), + "Tabbing order is visible for the top level target after clicking the checkbox" + ); + + const iframeTabingOrderHighlighterData = + await iframeHighlighterTestFront.getTabbingOrderHighlighterData( + iframeAccessibilityFrontActorID + ); + + is( + JSON.stringify(iframeTabingOrderHighlighterData), + JSON.stringify([`button#iframe-btn-1 : 2`, `button#iframe-btn-2 : 3`]), + "Tabbing order is visible for the top level target after clicking the checkbox" + ); + } else { + is( + JSON.stringify(tabbingOrderHighlighterData), + JSON.stringify([ + `button#top-btn-1 : 1`, + `html : 2`, + `button#iframe-btn-1 : 3`, + `button#iframe-btn-2 : 4`, + `button#top-btn-2 : 5`, + ]), + "Tabbing order is visible for the top level target after clicking the checkbox" + ); + } + + info("Select the iframe in the iframe picker"); + // Get the iframe picker items + const menuList = toolbox.doc.getElementById("toolbox-frame-menu"); + + if (isFissionEnabled() && !isEveryFrameTargetEnabled()) { + is( + menuList, + null, + "iframe picker does not show remote frames when Fission is enabled and EFT is disabled" + ); + return; + } + + const frames = Array.from(menuList.querySelectorAll(".command")); + + let onInitialized = win.once(win.EVENTS.INITIALIZED); + frames[1].click(); + await onInitialized; + await waitUntilState( + store, + state => state.ui.tabbingOrderDisplayed === false + ); + + is( + tabbingOrderCheckbox.checked, + false, + "Checkbox is unchecked after selecting an iframe" + ); + + tabbingOrderHighlighterData = + await topLevelFrameHighlighterTestFront.getTabbingOrderHighlighterData( + topLevelAccessibilityFrontActorID + ); + is( + tabbingOrderHighlighterData.length, + 0, + "Tabbing order is not visible anymore" + ); + + info( + `Click on "Show Tabbing Order" checkbox and check that highlighter is only displayed for selected frame` + ); + tabbingOrderCheckbox.click(); + await waitUntilState(store, state => state.ui.tabbingOrderDisplayed === true); + + tabbingOrderHighlighterData = + await topLevelFrameHighlighterTestFront.getTabbingOrderHighlighterData( + topLevelAccessibilityFrontActorID + ); + if (isFissionEnabled() || isEveryFrameTargetEnabled()) { + is( + tabbingOrderHighlighterData.length, + 0, + "There's no highlighter displayed on the top level target when focused on specific iframe" + ); + + const iframeTabingOrderHighlighterData = + await iframeHighlighterTestFront.getTabbingOrderHighlighterData( + iframeAccessibilityFrontActorID + ); + + is( + JSON.stringify(iframeTabingOrderHighlighterData), + JSON.stringify([`button#iframe-btn-1 : 1`, `button#iframe-btn-2 : 2`]), + "Tabbing order has expected data when a specific iframe is selected" + ); + } else { + // When Fission/EFT are not enabled, the highlighter is displayed from the top-level + // target, but only for the iframe + is( + JSON.stringify(tabbingOrderHighlighterData), + JSON.stringify([`button#iframe-btn-1 : 1`, `button#iframe-btn-2 : 2`]), + "Tabbing order has expected data when a specific iframe is selected" + ); + } + + info("Select the top level document back"); + onInitialized = win.once(win.EVENTS.INITIALIZED); + toolbox.doc.querySelector("#toolbox-frame-menu .command").click(); + await onInitialized; + + is( + tabbingOrderCheckbox.checked, + false, + "Checkbox is unchecked after selecting the top level frame" + ); + await waitUntilState( + store, + state => state.ui.tabbingOrderDisplayed === false + ); + + tabbingOrderHighlighterData = + await topLevelFrameHighlighterTestFront.getTabbingOrderHighlighterData( + topLevelAccessibilityFrontActorID + ); + + if (isFissionEnabled() || isEveryFrameTargetEnabled()) { + const iframeTabingOrderHighlighterData = + await iframeHighlighterTestFront.getTabbingOrderHighlighterData( + iframeAccessibilityFrontActorID + ); + + is( + iframeTabingOrderHighlighterData.length, + 0, + "Highlighter is hidden on the frame after selecting back the top level target" + ); + } + + await closeTabToolboxAccessibility(env.tab); +}); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree.js new file mode 100644 index 0000000000..d1be57ee2d --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree.js @@ -0,0 +1,75 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1>Top level header</h1> + <p>This is a paragraph.</p> + </body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Test the initial accessibility tree state.", + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + ], + }, + }, + { + desc: "Expand first tree node.", + setup: async ({ doc }) => toggleRow(doc, 0), + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "paragraph", + name: `""`, + }, + ], + }, + }, + { + desc: "Collapse first tree node.", + setup: async ({ doc }) => toggleRow(doc, 0), + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + ], + }, + }, +]; + +/** + * Simple test that checks content of the Accessibility panel tree. + */ +addA11yPanelTestsTask(tests, TEST_URI, "Test Accessibility panel tree."); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit.js new file mode 100644 index 0000000000..b321fafba2 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit.js @@ -0,0 +1,137 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global toggleRow, toggleMenuItem, TREE_FILTERS_MENU_ID */ + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1 style="color:rgba(255,0,0,0.1); background-color:rgba(255,255,255,1);"> + Top level header + </h1> + <h2 style="color:rgba(0,255,0,0.1); background-color:rgba(255,255,255,1);"> + Second level header + </h2> + </body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Expand first and second tree nodes.", + setup: async ({ doc }) => { + await toggleRow(doc, 0); + await toggleRow(doc, 1); + }, + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + level: 1, + }, + { + role: "heading", + name: `"Top level header"`, + level: 2, + }, + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + level: 3, + }, + { + role: "heading", + name: `"Second level header"`, + level: 2, + }, + ], + }, + }, + { + desc: "Click on the all filter.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: [ + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + level: 1, + }, + { + role: "text leaf", + name: `"Second level header "contrast`, + badges: ["contrast"], + selected: true, + level: 1, + }, + ], + }, + }, + { + desc: "Click on the all filter again.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + level: 1, + }, + { + role: "heading", + name: `"Top level header"`, + level: 2, + }, + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + level: 3, + }, + { + role: "heading", + name: `"Second level header"`, + level: 2, + }, + { + role: "text leaf", + name: `"Second level header "contrast`, + badges: ["contrast"], + selected: true, + level: 3, + }, + ], + }, + }, +]; + +/** + * Simple test that checks content of the Accessibility panel tree when one of + * the tree rows has a "contrast" badge and auditing is activated via toolbar + * filter. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree with contrast badge present." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_long.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_long.js new file mode 100644 index 0000000000..a8e295c504 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_long.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global toggleMenuItem, TREE_FILTERS_MENU_ID */ + +const header = + '<h1 style="color:rgba(255,0,0,0.1); ' + + 'background-color:rgba(255,255,255,1);">header</h1>'; + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + ${header.repeat(20)} + </body> +</html>`; + +const docRow = { + role: "document", + name: `"Accessibility Panel Test"`, +}; +const headingRow = { + role: "heading", + name: `"header"`, +}; +const textLeafRow = { + role: "text leaf", + name: `"header"contrast`, + badges: ["contrast"], +}; +const audit = new Array(20).fill(textLeafRow); + +const auditInitial = audit.map(check => ({ ...check })); +auditInitial[0].selected = true; + +const auditSecondLastSelected = audit.map(check => ({ ...check })); +auditSecondLastSelected[19].selected = true; + +const resetAfterAudit = [docRow]; +for (let i = 0; i < 20; i++) { + resetAfterAudit.push(headingRow); + resetAfterAudit.push({ ...textLeafRow, selected: i === 19 }); +} + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Check initial state.", + expected: { + tree: [{ ...docRow, selected: true }], + }, + }, + { + desc: "Run an audit from a11y panel toolbar by activating a filter.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: auditInitial, + }, + }, + { + desc: "Select a row that is guaranteed to have to be scrolled into view.", + setup: async ({ doc }) => { + selectRow(doc, 0); + EventUtils.synthesizeKey("VK_END", {}, doc.defaultView); + }, + expected: { + tree: auditSecondLastSelected, + }, + }, + { + desc: "Click on the filter again.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: resetAfterAudit, + }, + }, +]; + +/** + * Simple test that checks content of the Accessibility panel tree when the + * audit is activated via the panel's toolbar and the selection persists when + * the filter is toggled off. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree with persistent selected row." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_reset.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_reset.js new file mode 100644 index 0000000000..084a46dc89 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_reset.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global toggleMenuItem, selectAccessibleForNode, TREE_FILTERS_MENU_ID */ + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1 style="color:rgba(255,0,0,0.1); background-color:rgba(255,255,255,1);"> + Top level header + </h1> + <h2 style="color:rgba(0,255,0,0.1); background-color:rgba(255,255,255,1);"> + Second level header + </h2> + </body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Check initial state.", + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + selected: true, + }, + ], + }, + }, + { + desc: "Run an audit from a11y panel toolbar by activating a filter.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: [ + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + selected: true, + }, + { + role: "text leaf", + name: `"Second level header "contrast`, + badges: ["contrast"], + }, + ], + }, + }, + { + desc: "Select an accessible object.", + setup: async env => { + await selectAccessibleForNode(env, "h1"); + }, + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + selected: true, + }, + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + }, + { + role: "heading", + name: `"Second level header"`, + }, + { + role: "text leaf", + name: `"Second level header "contrast`, + badges: ["contrast"], + }, + ], + }, + }, +]; + +/** + * Simple test that checks content of the Accessibility panel tree when the + * audit is activated via the panel's toolbar. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree with contrast filter audit activation." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_toolbar.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_toolbar.js new file mode 100644 index 0000000000..9a1b9ba4c7 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_audit_toolbar.js @@ -0,0 +1,112 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* global toggleMenuItem, TREE_FILTERS_MENU_ID */ + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1 style="color:rgba(255,0,0,0.1); background-color:rgba(255,255,255,1);"> + Top level header + </h1> + <h2 style="color:rgba(0,255,0,0.1); background-color:rgba(255,255,255,1);"> + Second level header + </h2> + </body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Check initial state.", + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + selected: true, + }, + ], + activeToolbarFilters: [true, false, false, false, false], + }, + }, + { + desc: "Run an audit (all) from a11y panel toolbar by activating a filter.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: [ + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + selected: true, + }, + { + role: "text leaf", + name: `"Second level header "contrast`, + badges: ["contrast"], + }, + ], + activeToolbarFilters: [false, true, true, true, true], + }, + }, + { + desc: "Click on the filter again.", + setup: async ({ doc, toolbox }) => { + await toggleMenuItem(doc, toolbox.doc, TREE_FILTERS_MENU_ID, 1); + }, + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + selected: true, + }, + { + role: "heading", + name: `"Second level header"`, + }, + { + role: "text leaf", + name: `"Second level header "contrast`, + badges: ["contrast"], + }, + ], + activeToolbarFilters: [true, false, false, false, false], + }, + }, +]; + +/** + * Simple test that checks content of the Accessibility panel tree when the + * audit is activated via the panel's toolbar. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree with 'all' filter audit activation." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree_contrast.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree_contrast.js new file mode 100644 index 0000000000..2abfabd324 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_contrast.js @@ -0,0 +1,62 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1 style="color:rgba(255,0,0,0.1); background-color:rgba(255,255,255,1);"> + Top level header + </h1> + </body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Expand first and second tree nodes.", + setup: async ({ doc }) => { + await toggleRow(doc, 0); + await toggleRow(doc, 1); + }, + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "text leaf", + name: `"Top level header "contrast`, + badges: ["contrast"], + }, + ], + }, + }, +]; + +/** + * Simple test that checks content of the Accessibility panel tree when one of + * the tree rows has a "contrast" badge. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree with contrast badge." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree_iframe_picker.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree_iframe_picker.js new file mode 100644 index 0000000000..dc146a32d5 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_iframe_picker.js @@ -0,0 +1,121 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Check that the accessibility panel works as expected when using the iframe picker + +const TEST_URI = `data:text/html,<meta charset=utf8> + <head> + <title>TopLevel</title> + <style>h1 { color: lightgrey; }</style> + </head> + <body> + <h1>Top level header</h1> + <p>This is a paragraph.</p> + <iframe src="data:text/html,<meta charset=utf8> + <head> + <title>iframe</title> + <style>h2 { color: aliceblue }</style> + </head> + <body> + <h2>Iframe header</h2> + + <iframe src='data:text/html,<meta charset=utf8> + <head> + <title>nested iframe</title> + <style>h2 { color: lightpink }</style> + </head> + <body> + <h2>Nested Iframe header</h2> + </body> + '></iframe> + + </body> + "></iframe>`; + +add_task(async () => { + const env = await addTestTab(TEST_URI); + const { doc, toolbox, win } = env; + + await checkTree(env, [ + { + role: "document", + name: `"TopLevel"`, + }, + ]); + + info("Select the iframe in the iframe picker"); + // Get the iframe picker items + const menuList = toolbox.doc.getElementById("toolbox-frame-menu"); + const frames = Array.from(menuList.querySelectorAll(".command")); + + let onInitialized = win.once(win.EVENTS.INITIALIZED); + frames[1].click(); + await onInitialized; + + await checkTree(env, [ + { + role: "document", + name: `"iframe"`, + }, + ]); + + info( + "Run a constrast audit to check only issues from selected iframe tree are displayed" + ); + const CONTRAST_MENU_ITEM_INDEX = 2; + const onUpdated = win.once(win.EVENTS.ACCESSIBILITY_INSPECTOR_UPDATED); + await toggleMenuItem( + doc, + toolbox.doc, + TREE_FILTERS_MENU_ID, + CONTRAST_MENU_ITEM_INDEX + ); + await onUpdated; + // wait until the tree is filtered (i.e. the audit is done and only nodes with issues + // should be displayed) + await waitFor(() => doc.querySelector(".treeTable.filtered")); + + await checkTree(env, [ + { + role: "text leaf", + name: `"Iframe header"contrast`, + badges: ["contrast"], + level: 1, + }, + { + role: "text leaf", + name: `"Nested Iframe header"contrast`, + badges: ["contrast"], + level: 1, + }, + ]); + + info("Select the top level document back"); + onInitialized = win.once(win.EVENTS.INITIALIZED); + frames[0].click(); + await onInitialized; + + await checkTree(env, [ + { + role: "document", + name: `"TopLevel"`, + }, + ]); + + await closeTabToolboxAccessibility(env.tab); +}); + +function checkTree(env, tree) { + return runA11yPanelTests( + [ + { + expected: { + tree, + }, + }, + ], + env + ); +} diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree_navigation.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree_navigation.js new file mode 100644 index 0000000000..d98c3d8e35 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_navigation.js @@ -0,0 +1,186 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Panel Test</title> + </head> + <body> + <h1>Top level header</h1> + <p>This is a paragraph.</p> + </body> +</html>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Test the initial accessibility tree and sidebar states.", + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + ], + sidebar: { + name: "Accessibility Panel Test", + role: "document", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 2, + indexInParent: 0, + states: [ + // The focused state is an outdated state, since the toolbox should now + // have the focus and not the content page. See Bug 1702709. + "focused", + "readonly", + "focusable", + "opaque", + "enabled", + "sensitive", + ], + }, + }, + }, + { + desc: "Expand first tree node.", + setup: async ({ doc }) => toggleRow(doc, 0), + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "paragraph", + name: `""`, + }, + ], + }, + }, + { + desc: "Expand second tree node.", + setup: async ({ doc }) => toggleRow(doc, 1), + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "text leaf", + name: `"Top level header"`, + }, + { + role: "paragraph", + name: `""`, + }, + ], + sidebar: { + name: "Top level header", + role: "heading", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 1, + indexInParent: 0, + states: ["selectable text", "opaque", "enabled", "sensitive"], + }, + }, + }, + { + desc: "Select third tree node.", + setup: ({ doc }) => selectRow(doc, 2), + expected: { + sidebar: { + name: "Top level header", + role: "text leaf", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 0, + indexInParent: 0, + states: ["opaque", "enabled", "sensitive"], + }, + }, + }, + { + desc: "Collapse first tree node.", + setup: async ({ doc }) => toggleRow(doc, 0), + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + ], + sidebar: { + name: "Accessibility Panel Test", + role: "document", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 2, + indexInParent: 0, + states: ["readonly", "focusable", "opaque", "enabled", "sensitive"], + }, + }, + }, + { + desc: "Expand first tree node again.", + setup: async ({ doc }) => toggleRow(doc, 0), + expected: { + tree: [ + { + role: "document", + name: `"Accessibility Panel Test"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "text leaf", + name: `"Top level header"`, + }, + { + role: "paragraph", + name: `""`, + }, + ], + }, + }, +]; + +/** + * Check navigation within the tree. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree navigation." +); diff --git a/devtools/client/accessibility/test/browser/browser_accessibility_tree_navigation_oop.js b/devtools/client/accessibility/test/browser/browser_accessibility_tree_navigation_oop.js new file mode 100644 index 0000000000..dc710c82e4 --- /dev/null +++ b/devtools/client/accessibility/test/browser/browser_accessibility_tree_navigation_oop.js @@ -0,0 +1,149 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_URI = `<h1>Top level header</h1><p>This is a paragraph.</p>`; + +/** + * Test data 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. + * } + */ +const tests = [ + { + desc: "Test the initial accessibility tree and sidebar states.", + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + }, + ], + sidebar: { + name: "", + role: "document", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 1, + indexInParent: 0, + states: [ + // The focused state is an outdated state, since the toolbox should now + // have the focus and not the content page. See Bug 1702709. + "focused", + "readonly", + "focusable", + "opaque", + "enabled", + "sensitive", + ], + }, + }, + }, + { + desc: "Expand first tree node.", + setup: ({ doc }) => toggleRow(doc, 0), + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + }, + { + role: "internal frame", + name: `"Accessibility Panel Test (OOP)"`, + }, + ], + }, + }, + { + desc: "Expand second tree node. Display OOP document.", + setup: ({ doc }) => toggleRow(doc, 1), + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + }, + { + role: "internal frame", + name: `"Accessibility Panel Test (OOP)"`, + }, + { + role: "document", + name: `"Accessibility Panel Test (OOP)"`, + }, + ], + sidebar: { + name: "Accessibility Panel Test (OOP)", + role: "internal frame", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 1, + indexInParent: 0, + states: ["focusable", "opaque", "enabled", "sensitive"], + }, + }, + }, + { + desc: "Expand third tree node. Display OOP frame content.", + setup: ({ doc }) => toggleRow(doc, 2), + expected: { + tree: [ + { + role: "document", + name: `""text label`, + badges: ["text label"], + }, + { + role: "internal frame", + name: `"Accessibility Panel Test (OOP)"`, + }, + { + role: "document", + name: `"Accessibility Panel Test (OOP)"`, + }, + { + role: "heading", + name: `"Top level header"`, + }, + { + role: "paragraph", + name: `""`, + }, + ], + sidebar: { + name: "Accessibility Panel Test (OOP)", + role: "document", + actions: [], + value: "", + description: "", + keyboardShortcut: "", + childCount: 2, + indexInParent: 0, + states: ["readonly", "focusable", "opaque", "enabled", "sensitive"], + }, + }, + }, +]; + +/** + * Check navigation within the tree. + */ +addA11yPanelTestsTask( + tests, + TEST_URI, + "Test Accessibility panel tree navigation with OOP frame.", + { remoteIframe: true } +); 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); + }); +} |