From fbaf0bb26397aa498eb9156f06d5a6fe34dd7dd8 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Fri, 19 Apr 2024 03:14:29 +0200 Subject: Merging upstream version 125.0.1. Signed-off-by: Daniel Baumann --- .../BrowserTestUtils/BrowserTestUtils.sys.mjs | 36 +-- .../content/content-about-page-utils.js | 2 +- testing/mochitest/api.js | 2 +- .../browser_baselinecoverage_browser-chrome.js | 2 +- testing/mochitest/browser-test.js | 6 +- testing/mochitest/leaks.py | 14 +- testing/mochitest/server.js | 2 +- testing/mochitest/static/browser.template.txt | 2 +- .../SpecialPowersLoadChromeScript.js | 2 +- .../test_SpecialPowersExtension.html | 4 +- .../test_SpecialPowersLoadChromeScript.html | 2 +- ...est_SpecialPowersLoadChromeScript_function.html | 2 +- .../tests/Harness_sanity/test_createFiles.html | 6 +- .../Harness_sanity/test_sanityEventUtils.html | 4 +- .../tests/SimpleTest/AccessibilityUtils.js | 261 ++++++++++++--- testing/mochitest/tests/SimpleTest/EventUtils.js | 360 +++++++++++++++------ .../mochitest/tests/SimpleTest/LogController.js | 2 +- testing/mochitest/tests/SimpleTest/SimpleTest.js | 2 +- testing/mochitest/tests/SimpleTest/TestRunner.js | 2 +- .../mochitest/tests/SimpleTest/paint_listener.js | 2 +- testing/mochitest/tests/SimpleTest/setup.js | 4 +- .../tests/browser/browser_document_builder_sjs.js | 5 +- 22 files changed, 527 insertions(+), 197 deletions(-) (limited to 'testing/mochitest') diff --git a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs index 741882d9be..1793a6646f 100644 --- a/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs +++ b/testing/mochitest/BrowserTestUtils/BrowserTestUtils.sys.mjs @@ -704,7 +704,7 @@ export var BrowserTestUtils = { * @resolves When STATE_START reaches the tab's progress listener */ browserStarted(browser, expectedURI) { - let testFn = function (aStateFlags, aStatus) { + let testFn = function (aStateFlags) { return ( aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK && aStateFlags & Ci.nsIWebProgressListener.STATE_START @@ -763,7 +763,7 @@ export var BrowserTestUtils = { } else { urlMatches = urlToMatch => urlToMatch != "about:blank"; } - return new Promise((resolve, reject) => { + return new Promise(resolve => { tabbrowser.tabContainer.addEventListener( "TabOpen", function tabOpenListener(openEvent) { @@ -829,15 +829,9 @@ export var BrowserTestUtils = { * @resolves When onLocationChange fires. */ waitForLocationChange(tabbrowser, url) { - return new Promise((resolve, reject) => { + return new Promise(resolve => { let progressListener = { - onLocationChange( - aBrowser, - aWebProgress, - aRequest, - aLocationURI, - aFlags - ) { + onLocationChange(aBrowser, aWebProgress, aRequest, aLocationURI) { if ( (url && aLocationURI.spec != url) || (!url && aLocationURI.spec == "about:blank") @@ -885,7 +879,7 @@ export var BrowserTestUtils = { } return new Promise((resolve, reject) => { - let observe = async (win, topic, data) => { + let observe = async (win, topic) => { if (topic != "domwindowopened") { return; } @@ -1002,7 +996,7 @@ export var BrowserTestUtils = { */ domWindowOpened(win, checkFn) { return new Promise(resolve => { - async function observer(subject, topic, data) { + async function observer(subject, topic) { if (topic == "domwindowopened" && (!win || subject === win)) { let observedWindow = subject; if (checkFn && !(await checkFn(observedWindow))) { @@ -1055,7 +1049,7 @@ export var BrowserTestUtils = { */ domWindowClosed(win) { return new Promise(resolve => { - function observer(subject, topic, data) { + function observer(subject, topic) { if (topic == "domwindowclosed" && (!win || subject === win)) { Services.ww.unregisterNotification(observer); resolve(subject); @@ -1167,7 +1161,7 @@ export var BrowserTestUtils = { win.gBrowser._insertBrowser(win.gBrowser.getTabForBrowser(browser)); }); - let observer = (subject, topic, data) => { + let observer = subject => { if (browserSet.has(subject)) { browserSet.delete(subject); } @@ -1202,7 +1196,7 @@ export var BrowserTestUtils = { return new Promise(resolve => { let browser = tab.linkedBrowser; let flushTopic = "sessionstore-browser-shutdown-flush"; - let observer = (subject, topic, data) => { + let observer = subject => { if (subject === browser) { Services.obs.removeObserver(observer, flushTopic); // Wait for the next event tick to make sure other listeners are @@ -1615,7 +1609,7 @@ export var BrowserTestUtils = { } }, - observe(subject, topic, data) { + observe(subject, topic) { switch (topic) { case "test-complete": this._cleanupContentEventListeners(); @@ -2054,7 +2048,7 @@ export var BrowserTestUtils = { let expectedPromises = []; let crashCleanupPromise = new Promise((resolve, reject) => { - let observer = (subject, topic, data) => { + let observer = (subject, topic) => { if (topic != "ipc:content-shutdown") { reject("Received incorrect observer topic: " + topic); return; @@ -2129,7 +2123,7 @@ export var BrowserTestUtils = { if (shouldShowTabCrashPage) { expectedPromises.push( - new Promise((resolve, reject) => { + new Promise(resolve => { browser.addEventListener( "AboutTabCrashedReady", function onCrash() { @@ -2187,7 +2181,7 @@ export var BrowserTestUtils = { }); let sawNormalCrash = false; - let observer = (subject, topic, data) => { + let observer = () => { sawNormalCrash = true; }; @@ -2232,7 +2226,7 @@ export var BrowserTestUtils = { waitForAttribute(attr, element, value) { let MutationObserver = element.ownerGlobal.MutationObserver; return new Promise(resolve => { - let mut = new MutationObserver(mutations => { + let mut = new MutationObserver(() => { if ( (!value && element.hasAttribute(attr)) || (value && element.getAttribute(attr) === value) @@ -2263,7 +2257,7 @@ export var BrowserTestUtils = { let MutationObserver = element.ownerGlobal.MutationObserver; return new Promise(resolve => { dump("Waiting for removal\n"); - let mut = new MutationObserver(mutations => { + let mut = new MutationObserver(() => { if (!element.hasAttribute(attr)) { resolve(); mut.disconnect(); diff --git a/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js b/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js index e20100db4b..759e283bcb 100644 --- a/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js +++ b/testing/mochitest/BrowserTestUtils/content/content-about-page-utils.js @@ -14,7 +14,7 @@ function AboutPage(aboutHost, chromeURL, uriFlags) { AboutPage.prototype = { QueryInterface: ChromeUtils.generateQI(["nsIAboutModule"]), - getURIFlags(aURI) { + getURIFlags() { // eslint-disable-line no-unused-vars return this.uriFlags; }, diff --git a/testing/mochitest/api.js b/testing/mochitest/api.js index bd578f03b8..fa4422f290 100644 --- a/testing/mochitest/api.js +++ b/testing/mochitest/api.js @@ -26,7 +26,7 @@ const windowTracker = { Services.obs.addObserver(this, "chrome-document-global-created"); }, - async observe(window, topic, data) { + async observe(window, topic) { if (topic === "chrome-document-global-created") { await new Promise(resolve => window.addEventListener("DOMContentLoaded", resolve, { once: true }) diff --git a/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage_browser-chrome.js b/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage_browser-chrome.js index 4015bd6edf..3095934ffd 100644 --- a/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage_browser-chrome.js +++ b/testing/mochitest/baselinecoverage/browser_chrome/browser_baselinecoverage_browser-chrome.js @@ -13,7 +13,7 @@ add_task(async function () { gBrowser, url: "about:blank", }, - async function (browser) { + async function () { ok(true, "Collecting baseline coverage for browser-chrome tests."); await new Promise(c => setTimeout(c, 30 * 1000)); } diff --git a/testing/mochitest/browser-test.js b/testing/mochitest/browser-test.js index 98b9055605..1f9798bb36 100644 --- a/testing/mochitest/browser-test.js +++ b/testing/mochitest/browser-test.js @@ -45,7 +45,7 @@ var TabDestroyObserver = { Services.obs.removeObserver(this, "message-manager-disconnect"); }, - observe(subject, topic, data) { + observe(subject, topic) { if (topic == "message-manager-close") { this.outstanding.add(subject); } else if (topic == "message-manager-disconnect") { @@ -516,7 +516,7 @@ Tester.prototype = { this.SimpleTest.waitForFocus(aCallback); }, - finish: function Tester_finish(aSkipSummary) { + finish: function Tester_finish() { var passCount = this.tests.reduce((a, f) => a + f.passCount, 0); var failCount = this.tests.reduce((a, f) => a + f.failCount, 0); var todoCount = this.tests.reduce((a, f) => a + f.todoCount, 0); @@ -564,7 +564,7 @@ Tester.prototype = { this.repeat = 0; }, - observe: function Tester_observe(aSubject, aTopic, aData) { + observe: function Tester_observe(aSubject, aTopic) { if (!aTopic) { this.onConsoleMessage(aSubject); } diff --git a/testing/mochitest/leaks.py b/testing/mochitest/leaks.py index 76b6610963..08ed649a0c 100644 --- a/testing/mochitest/leaks.py +++ b/testing/mochitest/leaks.py @@ -8,8 +8,8 @@ import re from operator import itemgetter -RE_DOCSHELL = re.compile("I\/DocShellAndDOMWindowLeak ([+\-]{2})DOCSHELL") -RE_DOMWINDOW = re.compile("I\/DocShellAndDOMWindowLeak ([+\-]{2})DOMWINDOW") +RE_DOCSHELL = re.compile(r"I\/DocShellAndDOMWindowLeak ([+\-]{2})DOCSHELL") +RE_DOMWINDOW = re.compile(r"I\/DocShellAndDOMWindowLeak ([+\-]{2})DOMWINDOW") class ShutdownLeaks(object): @@ -233,7 +233,7 @@ class ShutdownLeaks(object): self.hiddenDocShellsCount += 1 def _parseValue(self, line, name): - match = re.search("\[%s = (.+?)\]" % name, line) + match = re.search(r"\[%s = (.+?)\]" % name, line) if match: return match.group(1) return None @@ -329,17 +329,17 @@ class LSANLeaks(object): ) self.startRegExp = re.compile( - "==\d+==ERROR: LeakSanitizer: detected memory leaks" + r"==\d+==ERROR: LeakSanitizer: detected memory leaks" ) self.fatalErrorRegExp = re.compile( - "==\d+==LeakSanitizer has encountered a fatal error." + r"==\d+==LeakSanitizer has encountered a fatal error." ) self.symbolizerOomRegExp = re.compile( "LLVMSymbolizer: error reading file: Cannot allocate memory" ) - self.stackFrameRegExp = re.compile(" #\d+ 0x[0-9a-f]+ in ([^( var eventCount = 0; -function testEventListener(e) { +function testEventListener() { ++eventCount; } -function testEventListener2(e) { +function testEventListener2() { ++eventCount; } diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript.html index 7242bd19f5..3c9bbf3913 100644 --- a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript.html +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript.html @@ -37,7 +37,7 @@ function endOfFirstTest() { // that is used to run the chrome script. script2 = SpecialPowers.loadChromeScript(_ => { /* eslint-env mozilla/chrome-script */ - addMessageListener("valid-assert", function (message) { + addMessageListener("valid-assert", function () { assert.equal(typeof XMLHttpRequest, "function", "XMLHttpRequest is defined"); assert.equal(typeof CSS, "undefined", "CSS is not defined"); sendAsyncMessage("valid-assert-done"); diff --git a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript_function.html b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript_function.html index af31d7b25a..4de074800f 100644 --- a/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript_function.html +++ b/testing/mochitest/tests/Harness_sanity/test_SpecialPowersLoadChromeScript_function.html @@ -22,7 +22,7 @@ var script = SpecialPowers.loadChromeScript(function loadChromeScriptTest() { sendAsyncMessage("bar", message); }); - addMessageListener("valid-assert", function (message) { + addMessageListener("valid-assert", function () { assert.ok(true, "valid assertion"); assert.equal(1, 1, "another valid assertion"); sendAsyncMessage("valid-assert-done"); diff --git a/testing/mochitest/tests/Harness_sanity/test_createFiles.html b/testing/mochitest/tests/Harness_sanity/test_createFiles.html index 0e0637a230..7a06a2ad11 100644 --- a/testing/mochitest/tests/Harness_sanity/test_createFiles.html +++ b/testing/mochitest/tests/Harness_sanity/test_createFiles.html @@ -23,7 +23,7 @@ is(f.type, fileType, "File should have the specified type"); test2(); }, - function (msg) { ok(false, "Should be able to create a file without an error"); test2(); } + function () { ok(false, "Should be able to create a file without an error"); test2(); } ); } @@ -36,7 +36,7 @@ SpecialPowers.createFiles([{name: "/\/\/\/\/\/\/\/\/\/\/\invalidname",}], function () { test3Check(false); }, - function (msg) { test3Check(true); } + function () { test3Check(true); } ); } @@ -75,7 +75,7 @@ ok(f.name, "test4 test file should have a name"); SimpleTest.finish(); }, - function (msg) { + function () { ok(false, "Should be able to create a file without a name without an error"); SimpleTest.finish(); } diff --git a/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html b/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html index b24251b227..4a9abb2305 100644 --- a/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html +++ b/testing/mochitest/tests/Harness_sanity/test_sanityEventUtils.html @@ -247,7 +247,7 @@ function starttest() { /* test synthesizeMouse* */ //focus trick enables us to run this in iframes - $("radioTarget1").addEventListener('focus', function (aEvent) { + $("radioTarget1").addEventListener('focus', function () { synthesizeMouse($("radioTarget1"), 1, 1, {}); is($("radioTarget1").checked, true, "synthesizeMouse should work") $("radioTarget1").checked = false; @@ -260,7 +260,7 @@ function starttest() { $("radioTarget1").focus(); //focus trick enables us to run this in iframes - $("textBoxA").addEventListener("focus", function (aEvent) { + $("textBoxA").addEventListener("focus", function () { check = false; $("textBoxA").addEventListener("click", function() { check = true; }, { once: true }); synthesizeMouseAtCenter($("textBoxA"), {}); diff --git a/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js index 0e95565019..cf4daaf416 100644 --- a/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js +++ b/testing/mochitest/tests/SimpleTest/AccessibilityUtils.js @@ -388,19 +388,23 @@ this.AccessibilityUtils = (function () { if (accessible.role != Ci.nsIAccessibleRole.ROLE_PAGETAB) { return false; // Not a tab. } - // ToDo: We may eventually need to support intervening generics between - // a tab and its tablist here. - const tablist = accessible.parent; + const tablist = findNonGenericParentAccessible(accessible); if (!tablist || tablist.role != Ci.nsIAccessibleRole.ROLE_PAGETABLIST) { return false; // The tab isn't inside a tablist. } // ToDo: We may eventually need to support tablists which use // aria-activedescendant here. // Check that there is only one keyboard reachable tab. - const childCount = tablist.childCount; let foundFocusable = false; - for (let c = 0; c < childCount; c++) { - const tab = tablist.getChildAt(c); + for (const tab of findNonGenericChildrenAccessible(tablist)) { + // Allow whitespaces to be included in the tablist for styling purposes + const isWhitespace = + tab.role == Ci.nsIAccessibleRole.ROLE_TEXT_LEAF && + tab.DOMNode.textContent.trim().length === 0; + if (tab.role != Ci.nsIAccessibleRole.ROLE_PAGETAB && !isWhitespace) { + // The tablist includes children other than tabs or whitespaces + a11yFail("Only tabs should be included in a tablist", accessible); + } // Use tabIndex rather than a11y focusable state because all tabs might // have tabindex="-1". if (tab.DOMNode.tabIndex == 0) { @@ -460,6 +464,72 @@ this.AccessibilityUtils = (function () { return node && XULElement.isInstance(node) && node.tagName == "tab"; } + /** + * The gridcells are not expected to be interactive and focusable + * individually, but it is allowed to manually manage focus within the grid + * per ARIA Grid pattern (https://www.w3.org/WAI/ARIA/apg/patterns/grid/). + * Example of such grid would be a datepicker where one gridcell can be + * selected and the focus is moved with arrow keys once the user tabbed into + * the grid. In grids like a calendar, only one element would be included in + * the focus order and the rest of grid cells may not have an interactive + * accessible created. We need to special case the check for these gridcells. + */ + function isAccessibleGridcell(node) { + if (!node || !node.ownerGlobal) { + return false; + } + const accessible = getAccessible(node); + + if (!accessible || accessible.role != Ci.nsIAccessibleRole.ROLE_GRID_CELL) { + return false; // Not a grid cell. + } + // ToDo: We may eventually need to support intervening generics between + // a grid cell and its grid container here. + const gridRow = accessible.parent; + if (!gridRow || gridRow.role != Ci.nsIAccessibleRole.ROLE_ROW) { + return false; // The grid cell isn't inside a row. + } + let grid = gridRow.parent; + if (!grid) { + return false; // The grid cell isn't inside a grid. + } + if (grid.role == Ci.nsIAccessibleRole.ROLE_GROUPING) { + // Grid built on the HTML table may include wrapper: + grid = grid.parent; + if (!grid || grid.role != Ci.nsIAccessibleRole.ROLE_GRID) { + return false; // The grid cell isn't inside a grid. + } + } + // Check that there is only one keyboard reachable grid cell. + let foundFocusable = false; + for (const gridCell of grid.DOMNode.querySelectorAll( + "td, [role=gridcell]" + )) { + // Grid cells are not expected to have a "tabindex" attribute and to be + // included in the focus order, with the exception of the only one cell + // that is included in the page tab sequence to provide access to the grid. + if (gridCell.tabIndex == 0) { + if (foundFocusable) { + // Only one grid cell within a grid should be focusable. + // ToDo: Fine-tune the a11y-check error message generated in this case. + // Strictly speaking, it's not ideal that we're performing an action + // from an is function, which normally only queries something without + // any externally observable behaviour. That said, fixing that would + // involve different return values for different cases (not a grid + // cell, too many focusable grid cells, etc) so we could move the + // a11yFail call to the caller. + a11yFail( + "Only one grid cell should be focusable in a grid", + accessible + ); + return false; + } + foundFocusable = true; + } + } + return foundFocusable; + } + /** * XUL treecol elements currently aren't focusable, making them inaccessible. * For now, we don't flag these as a failure to avoid breaking multiple tests. @@ -477,7 +547,7 @@ this.AccessibilityUtils = (function () { } /** - * Determine if an accessible is a combobox container of the url bar. We + * Determine if a DOM node is a combobox container of the url bar. We * intentionally leave this element unlabeled, because its child is a search * input that is the target and main control of this component. In general, we * want to avoid duplication in the label announcement when a user focuses the @@ -486,67 +556,124 @@ this.AccessibilityUtils = (function () { * difficult to keep the accessible name synchronized between the combobox and * the input. Thus, we need to special case the label check for this control. */ - function isUnlabeledUrlBarCombobox(accessible) { - const node = accessible.DOMNode; + function isUnlabeledUrlBarCombobox(node) { if (!node || !node.ownerGlobal) { return false; } - const ariaRoles = getAriaRoles(accessible); + let ariaRole = node.getAttribute("role"); // There are only two cases of this pattern: and const isMozInputBox = node.tagName == "moz-input-box" && node.classList.contains("urlbar-input-box"); const isSearchbar = node.tagName == "searchbar" && node.id == "searchbar"; - return (isMozInputBox || isSearchbar) && ariaRoles.includes("combobox"); + return (isMozInputBox || isSearchbar) && ariaRole == "combobox"; } /** - * Determine if an accessible is an option within the url bar. We know each + * Determine if a DOM node is an option within the url bar. We know each * url bar option is accessible, but it disappears as soon as it is clicked * during tests and the a11y-checks do not have time to test the label, * because the Fluent localization is not yet completed by then. Thus, we * need to special case the label check for these controls. */ - function isUnlabeledUrlBarOption(accessible) { - const node = accessible.DOMNode; + function isUnlabeledUrlBarOption(node) { if (!node || !node.ownerGlobal) { return false; } - const ariaRoles = getAriaRoles(accessible); - return ( + const role = getAccessible(node)?.role; + const isOption = node.tagName == "span" && - ariaRoles.includes("option") && - node.classList.contains("urlbarView-row-inner") && - node.hasAttribute("data-l10n-id") - ); + node.getAttribute("role") == "option" && + node.classList.contains("urlbarView-row-inner"); + const isMenuItem = + node.tagName == "menuitem" && + role == Ci.nsIAccessibleRole.ROLE_MENUITEM && + node.classList.contains("urlbarView-result-menuitem"); + // Not all options have "data-l10n-id" attributes in the URL Bar, because + // some of options are autocomplete options based on the user input and + // they are not expected to be localized. + return isOption || isMenuItem; } /** - * Determine if an accessible is a menuitem within the XUL menu. We know each + * Determine if a DOM node is a menuitem within the XUL menu. We know each * menuitem is accessible, but it disappears as soon as it is clicked during * tests and the a11y-checks do not have time to test the label, because the * Fluent localization is not yet completed by then. Thus, we need to special * case the label check for these controls. */ - function isUnlabeledMenuitem(accessible) { - const node = accessible.DOMNode; + function isUnlabeledMenuitem(node) { if (!node || !node.ownerGlobal) { return false; } - let hasLabel = false; - for (const child of node.childNodes) { - if (child.tagName == "label") { - hasLabel = true; - } - } + const hasLabel = node.querySelector("label, description"); + const isMenuItem = + node.getAttribute("role") == "menuitem" || + (node.tagName == "richlistitem" && + node.classList.contains("autocomplete-richlistitem")) || + (node.tagName == "menuitem" && + node.classList.contains("urlbarView-result-menuitem")); + + const isParentMenu = + node.parentNode.getAttribute("role") == "menu" || + (node.parentNode.tagName == "richlistbox" && + node.parentNode.classList.contains("autocomplete-richlistbox")) || + (node.parentNode.tagName == "menupopup" && + node.parentNode.classList.contains("urlbarView-result-menu")); return ( - accessible.role == Ci.nsIAccessibleRole.ROLE_MENUITEM && - accessible.parent.role == Ci.nsIAccessibleRole.ROLE_MENUPOPUP && + isMenuItem && + isParentMenu && hasLabel && - node.hasAttribute("data-l10n-id") + (node.hasAttribute("data-l10n-id") || node.tagName == "richlistitem") + ); + } + + /** + * Determine if the node is a "Show All" or one of image buttons on the + * about:config page, or a "X" close button on moz-message-bar. We know these + * buttons are accessible, but they disappear/are replaced as soon as they + * are clicked during tests and the a11y-checks do not have time to test the + * label, because the Fluent localization is not yet completed by then. + * Thus, we need to special case the label check for these controls. + */ + function isUnlabeledImageButton(node) { + if (!node || !node.ownerGlobal) { + return false; + } + const isShowAllButton = node.id == "show-all"; + const isReplacedImageButton = + node.classList.contains("button-add") || + node.classList.contains("button-delete") || + node.classList.contains("button-reset"); + const isCloseMozMessageBarButton = + node.classList.contains("close") && + node.getAttribute("data-l10n-id") == "moz-message-bar-close-button"; + return ( + node.tagName.toLowerCase() == "button" && + node.hasAttribute("data-l10n-id") && + (isShowAllButton || isReplacedImageButton || isCloseMozMessageBarButton) ); } + /** + * Determine if a node is a XUL:button on a prompt popup. We know this button + * is accessible, but it disappears as soon as it is clicked during tests and + * the a11y-checks do not have time to test the label, because the Fluent + * localization is not yet completed by then. Thus, we need to special case + * the label check for these controls. + */ + function isUnlabeledXulButton(node) { + if (!node || !node.ownerGlobal) { + return false; + } + const hasLabel = node.querySelector("label, xul\\:label"); + const isButton = + node.getAttribute("role") == "button" || + node.tagName == "button" || + node.tagName == "xul:button"; + return isButton && hasLabel && node.hasAttribute("data-l10n-id"); + } + /** * Determine if a node is a XUL element for which tabIndex should be ignored. * Some XUL elements report -1 for the .tabIndex property, even though they @@ -758,13 +885,6 @@ this.AccessibilityUtils = (function () { const { DOMNode } = accessible; let name = accessible.name; if (!name) { - if ( - isUnlabeledUrlBarCombobox(accessible) || - isUnlabeledUrlBarOption(accessible) || - isUnlabeledMenuitem(accessible) - ) { - return; - } // If text has just been inserted into the tree, the a11y engine might not // have picked it up yet. forceRefreshDriverTick(DOMNode); @@ -773,6 +893,21 @@ this.AccessibilityUtils = (function () { } catch (e) { // The Accessible died because the DOM node was removed or hidden. if (gEnv.labelRule) { + // Some elements disappear as soon as they are clicked during tests, + // their accessible dies before the Fluent localization is completed. + // We want to exclude these groups of nodes from the label check. + // Note: In other cases, this first block isn't necessarily hit + // because Fluent isn't finished yet. This might happen if a text + // node was inserted (whether by Fluent or something else) but a11y + // hasn't picked it up yet, but the node gets hidden before a11y + // can pick it up. + if ( + isUnlabeledUrlBarOption(DOMNode) || + isUnlabeledMenuitem(DOMNode) || + isUnlabeledImageButton(DOMNode) + ) { + return; + } a11yWarn("Unlabeled element removed before l10n finished", { DOMNode, }); @@ -795,6 +930,13 @@ this.AccessibilityUtils = (function () { accessible.name; } catch (e) { // The Accessible died because the DOM node was removed or hidden. + if ( + isUnlabeledUrlBarOption(DOMNode) || + isUnlabeledImageButton(DOMNode) || + isUnlabeledXulButton(DOMNode) + ) { + return; + } a11yWarn("Unlabeled element removed before l10n finished", { DOMNode, }); @@ -811,6 +953,15 @@ this.AccessibilityUtils = (function () { name = name.trim(); } if (gEnv.labelRule && !name) { + // The URL and Search Bar comboboxes are purposefully unlabeled, + // since they include labeled inputs that are receiving focus. + // Or the Accessible died because the DOM node was removed or hidden. + if ( + isUnlabeledUrlBarCombobox(DOMNode) || + isUnlabeledUrlBarOption(DOMNode) + ) { + return; + } a11yFail("Interactive elements must be labeled", accessible); return; @@ -935,6 +1086,36 @@ this.AccessibilityUtils = (function () { return null; } + /** + * Find the nearest non-generic ancestor for a node to account for generic + * containers to intervene between the ancestor and it child. + */ + function findNonGenericParentAccessible(childAcc) { + for (let acc = childAcc.parent; acc; acc = acc.parent) { + if (acc.computedARIARole != "generic") { + return acc; + } + } + return null; + } + + /** + * Find the nearest non-generic children for a node to account for generic + * containers to intervene between the ancestor and its children. + */ + function* findNonGenericChildrenAccessible(parentAcc) { + const count = parentAcc.childCount; + for (let c = 0; c < count; ++c) { + const child = parentAcc.getChildAt(c); + // When Gecko will consider only one role as generic, we'd use child.role + if (child.computedARIARole == "generic") { + yield* findNonGenericChildrenAccessible(child); + } else { + yield child; + } + } + } + function runIfA11YChecks(task) { return (...args) => (gA11YChecks ? task(...args) : null); } @@ -956,7 +1137,7 @@ this.AccessibilityUtils = (function () { // node might be the image. const acc = findInteractiveAccessible(node); if (!acc) { - if (isInaccessibleXulTreecol(node)) { + if (isAccessibleGridcell(node) || isInaccessibleXulTreecol(node)) { return; } if (gEnv.mustHaveAccessibleRule) { diff --git a/testing/mochitest/tests/SimpleTest/EventUtils.js b/testing/mochitest/tests/SimpleTest/EventUtils.js index 739c7052ea..8833b8bc59 100644 --- a/testing/mochitest/tests/SimpleTest/EventUtils.js +++ b/testing/mochitest/tests/SimpleTest/EventUtils.js @@ -1,3 +1,4 @@ +/* eslint-disable no-nested-ternary */ /** * EventUtils provides some utility methods for creating and sending DOM events. * @@ -569,14 +570,86 @@ function synthesizeMouse(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { aWindow ); } -function synthesizeTouch(aTarget, aOffsetX, aOffsetY, aEvent, aWindow) { - var rect = aTarget.getBoundingClientRect(); - return synthesizeTouchAtPoint( - rect.left + aOffsetX, - rect.top + aOffsetY, - aEvent, - aWindow - ); + +/** + * Synthesize one or more touches on aTarget. aTarget can be either Element + * or Array of Elements. aOffsetX, aOffsetY, aEvent.id, aEvent.rx, aEvent.ry, + * aEvent.angle, aEvent.force, aEvent.tiltX, aEvent.tiltY and aEvent.twist can + * be either Number or Array of Numbers (can be mixed). If you specify array + * to synthesize a multi-touch, you need to specify same length arrays. If + * you don't specify array to them, same values (or computed default values for + * aEvent.id) are used for all touches. + * + * @param {Element | Element[]} aTarget The target element which you specify + * relative offset from its top-left. + * @param {Number | Number[]} aOffsetX The relative offset from left of aTarget. + * @param {Number | Number[]} aOffsetY The relative offset from top of aTarget. + * @param {Object} aEvent + * type: The touch event type. If undefined, "touchstart" and "touchend" will + * be synthesized at same point. + * + * id: The touch id. If you don't specify this, default touch id will be used + * for first touch and further touch ids are the values incremented from the + * first id. + * + * rx, ry: The radii of the touch. + * + * angle: The angle in degree. + * + * force: The force of the touch. If the type is "touchend", this should be 0. + * If unspecified, this is default to 0 for "touchend" or 1 for the others. + * + * tiltX, tiltY: The tilt of the touch. + * + * twist: The twist of the touch. + * @param {Window} aWindow Default to `window`. + * @returns true if and only if aEvent.type is specified and default of the + * event is prevented. + */ +function synthesizeTouch( + aTarget, + aOffsetX, + aOffsetY, + aEvent = {}, + aWindow = window +) { + let rectX, rectY; + if (Array.isArray(aTarget)) { + let lastTarget, lastTargetRect; + aTarget.forEach(target => { + const rect = + target == lastTarget ? lastTargetRect : target.getBoundingClientRect(); + rectX.push(rect.left); + rectY.push(rect.top); + lastTarget = target; + lastTargetRect = rect; + }); + } else { + const rect = aTarget.getBoundingClientRect(); + rectX = [rect.left]; + rectY = [rect.top]; + } + const offsetX = (() => { + if (Array.isArray(aOffsetX)) { + let ret = []; + aOffsetX.forEach((value, index) => { + ret.push(value + rectX[Math.min(index, rectX.length - 1)]); + }); + return ret; + } + return aOffsetX + rectX[0]; + })(); + const offsetY = (() => { + if (Array.isArray(aOffsetY)) { + let ret = []; + aOffsetY.forEach((value, index) => { + ret.push(value + rectY[Math.min(index, rectY.length - 1)]); + }); + return ret; + } + return aOffsetY + rectY[0]; + })(); + return synthesizeTouchAtPoint(offsetX, offsetY, aEvent, aWindow); } /** @@ -776,68 +849,160 @@ function synthesizeMouseAtPoint(left, top, aEvent, aWindow = window) { return defaultPrevented; } -function synthesizeTouchAtPoint(left, top, aEvent, aWindow = window) { - var utils = _getDOMWindowUtils(aWindow); - let defaultPrevented = false; +/** + * Synthesize one or more touches at the points. aLeft, aTop, aEvent.id, + * aEvent.rx, aEvent.ry, aEvent.angle, aEvent.force, aEvent.tiltX, aEvent.tiltY + * and aEvent.twist can be either Number or Array of Numbers (can be mixed). + * If you specify array to synthesize a multi-touch, you need to specify same + * length arrays. If you don't specify array to them, same values are used for + * all touches. + * + * @param {Element | Element[]} aTarget The target element which you specify + * relative offset from its top-left. + * @param {Number | Number[]} aOffsetX The relative offset from left of aTarget. + * @param {Number | Number[]} aOffsetY The relative offset from top of aTarget. + * @param {Object} aEvent + * type: The touch event type. If undefined, "touchstart" and "touchend" will + * be synthesized at same point. + * + * id: The touch id. If you don't specify this, default touch id will be used + * for first touch and further touch ids are the values incremented from the + * first id. + * + * rx, ry: The radii of the touch. + * + * angle: The angle in degree. + * + * force: The force of the touch. If the type is "touchend", this should be 0. + * If unspecified, this is default to 0 for "touchend" or 1 for the others. + * + * tiltX, tiltY: The tilt of the touch. + * + * twist: The twist of the touch. + * @param {Window} aWindow Default to `window`. + * @returns true if and only if aEvent.type is specified and default of the + * event is prevented. + */ +function synthesizeTouchAtPoint(aLeft, aTop, aEvent = {}, aWindow = window) { + let utils = _getDOMWindowUtils(aWindow); + if (!utils) { + return false; + } - if (utils) { - var id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID; - var rx = aEvent.rx || 1; - var ry = aEvent.ry || 1; - var angle = aEvent.angle || 0; - var force = aEvent.force || (aEvent.type === "touchend" ? 0 : 1); - var tiltX = aEvent.tiltX || 0; - var tiltY = aEvent.tiltY || 0; - var twist = aEvent.twist || 0; - var modifiers = _parseModifiers(aEvent, aWindow); + if ( + Array.isArray(aLeft) && + Array.isArray(aTop) && + aLeft.length != aTop.length + ) { + throw new Error(`aLeft and aTop should be same length array`); + } - if ("type" in aEvent && aEvent.type) { - defaultPrevented = utils.sendTouchEvent( - aEvent.type, - [id], - [left], - [top], - [rx], - [ry], - [angle], - [force], - [tiltX], - [tiltY], - [twist], - modifiers - ); - } else { - utils.sendTouchEvent( - "touchstart", - [id], - [left], - [top], - [rx], - [ry], - [angle], - [force], - [tiltX], - [tiltY], - [twist], - modifiers - ); - utils.sendTouchEvent( - "touchend", - [id], - [left], - [top], - [rx], - [ry], - [angle], - [force], - [tiltX], - [tiltY], - [twist], - modifiers + const arrayLength = Array.isArray(aLeft) + ? aLeft.length + : Array.isArray(aTop) + ? aTop.length + : 1; + + function throwExceptionIfDifferentLengthArray(aArray, aName) { + if (Array.isArray(aArray) && arrayLength !== aArray.length) { + throw new Error(`${aName} is different length array`); + } + } + const leftArray = (() => { + if (Array.isArray(aLeft)) { + return aLeft; + } + return new Array(arrayLength).fill(aLeft); + })(); + const topArray = (() => { + if (Array.isArray(aTop)) { + throwExceptionIfDifferentLengthArray(aTop, "aTop"); + return aTop; + } + return new Array(arrayLength).fill(aTop); + })(); + const idArray = (() => { + if ("id" in aEvent && Array.isArray(aEvent.id)) { + throwExceptionIfDifferentLengthArray(aEvent.id, "aEvent.id"); + return aEvent.id; + } + let id = aEvent.id || utils.DEFAULT_TOUCH_POINTER_ID; + let ret = []; + for (let i = 0; i < arrayLength; i++) { + ret.push(id++); + } + return ret; + })(); + function getSameLengthArrayOfEventProperty(aProperty, aDefaultValue) { + if (aProperty in aEvent && Array.isArray(aEvent[aProperty])) { + throwExceptionIfDifferentLengthArray( + aEvent.rx, + arrayLength, + `aEvent.${aProperty}` ); + return aEvent[aProperty]; } + return new Array(arrayLength).fill(aEvent[aProperty] || aDefaultValue); } - return defaultPrevented; + const rxArray = getSameLengthArrayOfEventProperty("rx", 1); + const ryArray = getSameLengthArrayOfEventProperty("ry", 1); + const angleArray = getSameLengthArrayOfEventProperty("angle", 0); + const forceArray = getSameLengthArrayOfEventProperty( + "force", + aEvent.type === "touchend" ? 0 : 1 + ); + const tiltXArray = getSameLengthArrayOfEventProperty("tiltX", 0); + const tiltYArray = getSameLengthArrayOfEventProperty("tiltY", 0); + const twistArray = getSameLengthArrayOfEventProperty("twist", 0); + + const modifiers = _parseModifiers(aEvent, aWindow); + + if ("type" in aEvent && aEvent.type) { + return utils.sendTouchEvent( + aEvent.type, + idArray, + leftArray, + topArray, + rxArray, + ryArray, + angleArray, + forceArray, + tiltXArray, + tiltYArray, + twistArray, + modifiers + ); + } + + utils.sendTouchEvent( + "touchstart", + idArray, + leftArray, + topArray, + rxArray, + ryArray, + angleArray, + forceArray, + tiltXArray, + tiltYArray, + twistArray, + modifiers + ); + utils.sendTouchEvent( + "touchend", + idArray, + leftArray, + topArray, + rxArray, + ryArray, + angleArray, + forceArray, + tiltXArray, + tiltYArray, + twistArray, + modifiers + ); + return false; } // Call synthesizeMouse with coordinates at the center of aTarget. @@ -851,7 +1016,7 @@ function synthesizeMouseAtCenter(aTarget, aEvent, aWindow) { aWindow ); } -function synthesizeTouchAtCenter(aTarget, aEvent, aWindow) { +function synthesizeTouchAtCenter(aTarget, aEvent = {}, aWindow = window) { var rect = aTarget.getBoundingClientRect(); synthesizeTouchAtPoint( rect.left + rect.width / 2, @@ -1020,7 +1185,9 @@ function _sendWheelAndPaint( } var onwheel = function () { - SpecialPowers.removeSystemEventListener(window, "wheel", onwheel); + SpecialPowers.wrap(window).removeEventListener("wheel", onwheel, { + mozSystemGroup: true, + }); // Wait one frame since the wheel event has not caused a refresh observer // to be added yet. @@ -1055,7 +1222,9 @@ function _sendWheelAndPaint( // Listen for the system wheel event, because it happens after all of // the other wheel events, including legacy events. - SpecialPowers.addSystemEventListener(aWindow, "wheel", onwheel); + SpecialPowers.wrap(aWindow).addEventListener("wheel", onwheel, { + mozSystemGroup: true, + }); if (aFlushMode === _FlushModes.FLUSH) { synthesizeWheel(aTarget, aOffsetX, aOffsetY, aEvent, aWindow); } else { @@ -1405,13 +1574,10 @@ function synthesizeAndWaitNativeMouseMove( ); let eventRegisteredPromise = new Promise(resolve => { - mm.addMessageListener( - "Test:MouseMoveRegistered", - function processed(message) { - mm.removeMessageListener("Test:MouseMoveRegistered", processed); - resolve(); - } - ); + mm.addMessageListener("Test:MouseMoveRegistered", function processed() { + mm.removeMessageListener("Test:MouseMoveRegistered", processed); + resolve(); + }); }); let eventReceivedPromise = ContentTask.spawn( browser, @@ -1579,7 +1745,7 @@ function synthesizeAndWaitKey( ); let keyRegisteredPromise = new Promise(resolve => { - mm.addMessageListener("Test:KeyRegistered", function processed(message) { + mm.addMessageListener("Test:KeyRegistered", function processed() { mm.removeMessageListener("Test:KeyRegistered", processed); resolve(); }); @@ -3853,21 +4019,19 @@ async function synthesizePlainDragAndCancel( aExpectedDataTransferItems ); } - SpecialPowers.addSystemEventListener( - srcElement.ownerDocument, + SpecialPowers.wrap(srcElement.ownerDocument).addEventListener( "dragstart", onDragStart, - { capture: true } + { capture: true, mozSystemGroup: true } ); try { aParams.expectCancelDragStart = true; await synthesizePlainDragAndDrop(aParams); } finally { - SpecialPowers.removeSystemEventListener( - srcElement.ownerDocument, + SpecialPowers.wrap(srcElement.ownerDocument).removeEventListener( "dragstart", onDragStart, - { capture: true } + { capture: true, mozSystemGroup: true } ); } return result; @@ -3884,33 +4048,23 @@ class EventCounter { // SpecialPowers is picky and needs to be passed an explicit reference to // the function to be called. To avoid having to bind "this", we therefore // define the method this way, via a property. - this.handleEvent = aEvent => { + this.handleEvent = () => { this.eventCount++; }; - if (aOptions.mozSystemGroup) { - SpecialPowers.addSystemEventListener( - aTarget, - aType, - this.handleEvent, - aOptions.capture - ); - } else { - aTarget.addEventListener(aType, this, aOptions); - } + SpecialPowers.wrap(aTarget).addEventListener( + aType, + this.handleEvent, + aOptions + ); } unregister() { - if (this.options.mozSystemGroup) { - SpecialPowers.removeSystemEventListener( - this.target, - this.type, - this.handleEvent, - this.options.capture - ); - } else { - this.target.removeEventListener(this.type, this, this.options); - } + SpecialPowers.wrap(this.target).removeEventListener( + this.type, + this.handleEvent, + this.options + ); } get count() { diff --git a/testing/mochitest/tests/SimpleTest/LogController.js b/testing/mochitest/tests/SimpleTest/LogController.js index 29580022f8..d0888d6032 100644 --- a/testing/mochitest/tests/SimpleTest/LogController.js +++ b/testing/mochitest/tests/SimpleTest/LogController.js @@ -48,7 +48,7 @@ LogController.extend = function (args, skip) { }; /* logs message with given level. Currently used locally by log() and error() */ -LogController.logWithLevel = function (level, message /*, ...*/) { +LogController.logWithLevel = function (level /*, ...*/) { var msg = LogController.createLogMessage( level, LogController.extend(arguments, 1) diff --git a/testing/mochitest/tests/SimpleTest/SimpleTest.js b/testing/mochitest/tests/SimpleTest/SimpleTest.js index a237c25103..16f5c3ae8a 100644 --- a/testing/mochitest/tests/SimpleTest/SimpleTest.js +++ b/testing/mochitest/tests/SimpleTest/SimpleTest.js @@ -384,7 +384,7 @@ function usesFailurePatterns() { * @return {boolean} Whether a matched failure pattern is found. */ function recordIfMatchesFailurePattern(name, diag) { - let index = SimpleTest.expected.findIndex(([pat, count]) => { + let index = SimpleTest.expected.findIndex(([pat]) => { return ( pat == null || (typeof name == "string" && name.includes(pat)) || diff --git a/testing/mochitest/tests/SimpleTest/TestRunner.js b/testing/mochitest/tests/SimpleTest/TestRunner.js index 5f305a176b..2e48969d8d 100644 --- a/testing/mochitest/tests/SimpleTest/TestRunner.js +++ b/testing/mochitest/tests/SimpleTest/TestRunner.js @@ -63,7 +63,7 @@ function extend(obj, /* optional */ skip) { return ret; } -function flattenArguments(lst /* ...*/) { +function flattenArguments(/* ...*/) { var res = []; var args = extend(arguments); while (args.length) { diff --git a/testing/mochitest/tests/SimpleTest/paint_listener.js b/testing/mochitest/tests/SimpleTest/paint_listener.js index 2fc6ab425a..89740454f2 100644 --- a/testing/mochitest/tests/SimpleTest/paint_listener.js +++ b/testing/mochitest/tests/SimpleTest/paint_listener.js @@ -100,7 +100,7 @@ window.promiseAllPaintsDone = function (subdoc = null, flush = false) { var flushmode = flush ? FlushModes.FLUSH : FlushModes.NOFLUSH; - return new Promise(function (resolve, reject) { + return new Promise(function (resolve) { // The callback is given the components of the rect, but resolve() can // only be given one arg, so we turn it back into an array. waitForPaints((l, r, t, b) => resolve([l, r, t, b]), subdoc, flushmode); diff --git a/testing/mochitest/tests/SimpleTest/setup.js b/testing/mochitest/tests/SimpleTest/setup.js index 05ba8066a1..2a8509bc0a 100644 --- a/testing/mochitest/tests/SimpleTest/setup.js +++ b/testing/mochitest/tests/SimpleTest/setup.js @@ -247,7 +247,7 @@ TestRunner.logger.addListener( var gTestList = []; var RunSet = {}; -RunSet.runall = function (e) { +RunSet.runall = function () { // Filter tests to include|exclude tests based on data in params.filter. // This allows for including or excluding tests from the gTestList // TODO Only used by ipc tests, remove once those are implemented sanely @@ -265,7 +265,7 @@ RunSet.runall = function (e) { } }; -RunSet.runtests = function (e) { +RunSet.runtests = function () { // Which tests we're going to run var my_tests = gTestList; diff --git a/testing/mochitest/tests/browser/browser_document_builder_sjs.js b/testing/mochitest/tests/browser/browser_document_builder_sjs.js index 4b653a792d..fdf1691b7f 100644 --- a/testing/mochitest/tests/browser/browser_document_builder_sjs.js +++ b/testing/mochitest/tests/browser/browser_document_builder_sjs.js @@ -23,8 +23,9 @@ add_task(async function assertHtmlParam() { const duration = performance.now() - startTime; is(response.status, 200, "Response is a 200"); - ok( - duration > delay, + Assert.greater( + duration, + delay, `The delay parameter works as expected (took ${duration}ms)` ); -- cgit v1.2.3