diff options
Diffstat (limited to '')
157 files changed, 21215 insertions, 0 deletions
diff --git a/accessible/tests/browser/.eslintrc.js b/accessible/tests/browser/.eslintrc.js new file mode 100644 index 0000000000..528797cb91 --- /dev/null +++ b/accessible/tests/browser/.eslintrc.js @@ -0,0 +1,28 @@ +"use strict"; + +module.exports = { + rules: { + "mozilla/no-aArgs": "error", + "mozilla/reject-importGlobalProperties": ["error", "everything"], + "mozilla/var-only-at-top-level": "error", + + "block-scoped-var": "error", + camelcase: ["error", { properties: "never" }], + complexity: ["error", 20], + + "handle-callback-err": ["error", "er"], + "max-nested-callbacks": ["error", 4], + "new-cap": ["error", { capIsNew: false }], + "no-fallthrough": "error", + "no-multi-str": "error", + "no-proto": "error", + "no-return-assign": "error", + "no-shadow": "error", + "no-unused-vars": ["error", { vars: "all", args: "none" }], + "one-var": ["error", "never"], + radix: "error", + strict: ["error", "global"], + yoda: "error", + "no-undef-init": "error", + }, +}; diff --git a/accessible/tests/browser/Common.jsm b/accessible/tests/browser/Common.jsm new file mode 100644 index 0000000000..527e301be9 --- /dev/null +++ b/accessible/tests/browser/Common.jsm @@ -0,0 +1,456 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["CommonUtils"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); + +const MAX_TRIM_LENGTH = 100; + +const CommonUtils = { + /** + * Constant passed to getAccessible to indicate that it shouldn't fail if + * there is no accessible. + */ + DONOTFAIL_IF_NO_ACC: 1, + + /** + * Constant passed to getAccessible to indicate that it shouldn't fail if it + * does not support an interface. + */ + DONOTFAIL_IF_NO_INTERFACE: 2, + + /** + * nsIAccessibilityService service. + */ + get accService() { + if (!this._accService) { + this._accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + } + + return this._accService; + }, + + clearAccService() { + this._accService = null; + Cu.forceGC(); + }, + + /** + * Adds an observer for an 'a11y-consumers-changed' event. + */ + addAccConsumersChangedObserver() { + const deferred = {}; + this._accConsumersChanged = new Promise(resolve => { + deferred.resolve = resolve; + }); + const observe = (subject, topic, data) => { + Services.obs.removeObserver(observe, "a11y-consumers-changed"); + deferred.resolve(JSON.parse(data)); + }; + Services.obs.addObserver(observe, "a11y-consumers-changed"); + }, + + /** + * Returns a promise that resolves when 'a11y-consumers-changed' event is + * fired. + * + * @return {Promise} + * event promise evaluating to event's data + */ + observeAccConsumersChanged() { + return this._accConsumersChanged; + }, + + /** + * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "1" + * which indicates that an accessibility service is initialized in the current + * process. + */ + addAccServiceInitializedObserver() { + const deferred = {}; + this._accServiceInitialized = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + const observe = (subject, topic, data) => { + if (data === "1") { + Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); + deferred.resolve(); + } else { + deferred.reject("Accessibility service is shutdown unexpectedly."); + } + }; + Services.obs.addObserver(observe, "a11y-init-or-shutdown"); + }, + + /** + * Returns a promise that resolves when an accessibility service is + * initialized in the current process. Otherwise (if the service is shutdown) + * the promise is rejected. + */ + observeAccServiceInitialized() { + return this._accServiceInitialized; + }, + + /** + * Adds an observer for an 'a11y-init-or-shutdown' event with a value of "0" + * which indicates that an accessibility service is shutdown in the current + * process. + */ + addAccServiceShutdownObserver() { + const deferred = {}; + this._accServiceShutdown = new Promise((resolve, reject) => { + deferred.resolve = resolve; + deferred.reject = reject; + }); + const observe = (subject, topic, data) => { + if (data === "0") { + Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); + deferred.resolve(); + } else { + deferred.reject("Accessibility service is initialized unexpectedly."); + } + }; + Services.obs.addObserver(observe, "a11y-init-or-shutdown"); + }, + + /** + * Returns a promise that resolves when an accessibility service is shutdown + * in the current process. Otherwise (if the service is initialized) the + * promise is rejected. + */ + observeAccServiceShutdown() { + return this._accServiceShutdown; + }, + + /** + * Extract DOMNode id from an accessible. If the accessible is in the remote + * process, DOMNode is not present in parent process. However, if specified by + * the author, DOMNode id will be attached to an accessible object. + * + * @param {nsIAccessible} accessible accessible + * @return {String?} DOMNode id if available + */ + getAccessibleDOMNodeID(accessible) { + if (accessible instanceof Ci.nsIAccessibleDocument) { + // If accessible is a document, trying to find its document body id. + try { + return accessible.DOMNode.body.id; + } catch (e) { + /* This only works if accessible is not a proxy. */ + } + } + try { + return accessible.DOMNode.id; + } catch (e) { + /* This will fail if DOMNode is in different process. */ + } + try { + // When e10s is enabled, accessible will have an "id" property if its + // corresponding DOMNode has an id. If accessible is a document, its "id" + // property corresponds to the "id" of its body element. + return accessible.id; + } catch (e) { + /* This will fail if accessible is not a proxy. */ + } + + return null; + }, + + getObjAddress(obj) { + const exp = /native\s*@\s*(0x[a-f0-9]+)/g; + const match = exp.exec(obj.toString()); + if (match) { + return match[1]; + } + + return obj.toString(); + }, + + getNodePrettyName(node) { + try { + let tag = ""; + if (node.nodeType == Node.DOCUMENT_NODE) { + tag = "document"; + } else { + tag = node.localName; + if (node.nodeType == Node.ELEMENT_NODE && node.hasAttribute("id")) { + tag += `@id="${node.getAttribute("id")}"`; + } + } + + return `"${tag} node", address: ${this.getObjAddress(node)}`; + } catch (e) { + return `" no node info "`; + } + }, + + /** + * Convert role to human readable string. + */ + roleToString(role) { + return this.accService.getStringRole(role); + }, + + /** + * Shorten a long string if it exceeds MAX_TRIM_LENGTH. + * + * @param aString the string to shorten. + * + * @returns the shortened string. + */ + shortenString(str) { + if (str.length <= MAX_TRIM_LENGTH) { + return str; + } + + // Trim the string if its length is > MAX_TRIM_LENGTH characters. + const trimOffset = MAX_TRIM_LENGTH / 2; + + return `${str.substring(0, trimOffset - 1)}…${str.substring( + str.length - trimOffset, + str.length + )}`; + }, + + normalizeAccTreeObj(obj) { + const key = Object.keys(obj)[0]; + const roleName = `ROLE_${key}`; + if (roleName in Ci.nsIAccessibleRole) { + return { + role: Ci.nsIAccessibleRole[roleName], + children: obj[key], + }; + } + + return obj; + }, + + stringifyTree(obj) { + let text = this.roleToString(obj.role) + ": [ "; + if ("children" in obj) { + for (let i = 0; i < obj.children.length; i++) { + const c = this.normalizeAccTreeObj(obj.children[i]); + text += this.stringifyTree(c); + if (i < obj.children.length - 1) { + text += ", "; + } + } + } + + return `${text}] `; + }, + + /** + * Return pretty name for identifier, it may be ID, DOM node or accessible. + */ + prettyName(identifier) { + if (identifier instanceof Array) { + let msg = ""; + for (let idx = 0; idx < identifier.length; idx++) { + if (msg != "") { + msg += ", "; + } + + msg += this.prettyName(identifier[idx]); + } + return msg; + } + + if (identifier instanceof Ci.nsIAccessible) { + const acc = this.getAccessible(identifier); + const domID = this.getAccessibleDOMNodeID(acc); + let msg = "["; + try { + if (Services.appinfo.browserTabsRemoteAutostart) { + if (domID) { + msg += `DOM node id: ${domID}, `; + } + } else { + msg += `${this.getNodePrettyName(acc.DOMNode)}, `; + } + msg += `role: ${this.roleToString(acc.role)}`; + if (acc.name) { + msg += `, name: "${this.shortenString(acc.name)}"`; + } + } catch (e) { + msg += "defunct"; + } + + if (acc) { + msg += `, address: ${this.getObjAddress(acc)}`; + } + msg += "]"; + + return msg; + } + + if (Node.isInstance(identifier)) { + return `[ ${this.getNodePrettyName(identifier)} ]`; + } + + if (identifier && typeof identifier === "object") { + const treeObj = this.normalizeAccTreeObj(identifier); + if ("role" in treeObj) { + return `{ ${this.stringifyTree(treeObj)} }`; + } + + return JSON.stringify(identifier); + } + + return ` "${identifier}" `; + }, + + /** + * Return accessible for the given identifier (may be ID attribute or DOM + * element or accessible object) or null. + * + * @param accOrElmOrID + * identifier to get an accessible implementing the given interfaces + * @param aInterfaces + * [optional] the interface or an array interfaces to query it/them + * from obtained accessible + * @param elmObj + * [optional] object to store DOM element which accessible is obtained + * for + * @param doNotFailIf + * [optional] no error for special cases (see DONOTFAIL_IF_NO_ACC, + * DONOTFAIL_IF_NO_INTERFACE) + * @param doc + * [optional] document for when accOrElmOrID is an ID. + */ + getAccessible(accOrElmOrID, interfaces, elmObj, doNotFailIf, doc) { + if (!accOrElmOrID) { + return null; + } + + let elm = null; + if (accOrElmOrID instanceof Ci.nsIAccessible) { + try { + elm = accOrElmOrID.DOMNode; + } catch (e) {} + } else if (Node.isInstance(accOrElmOrID)) { + elm = accOrElmOrID; + } else { + elm = doc.getElementById(accOrElmOrID); + if (!elm) { + Assert.ok(false, `Can't get DOM element for ${accOrElmOrID}`); + return null; + } + } + + if (elmObj && typeof elmObj == "object") { + elmObj.value = elm; + } + + let acc = accOrElmOrID instanceof Ci.nsIAccessible ? accOrElmOrID : null; + if (!acc) { + try { + acc = this.accService.getAccessibleFor(elm); + } catch (e) {} + + if (!acc) { + if (!(doNotFailIf & this.DONOTFAIL_IF_NO_ACC)) { + Assert.ok( + false, + `Can't get accessible for ${this.prettyName(accOrElmOrID)}` + ); + } + + return null; + } + } + + if (!interfaces) { + return acc; + } + + if (!(interfaces instanceof Array)) { + interfaces = [interfaces]; + } + + for (let index = 0; index < interfaces.length; index++) { + if (acc instanceof interfaces[index]) { + continue; + } + + try { + acc.QueryInterface(interfaces[index]); + } catch (e) { + if (!(doNotFailIf & this.DONOTFAIL_IF_NO_INTERFACE)) { + Assert.ok( + false, + `Can't query ${interfaces[index]} for ${accOrElmOrID}` + ); + } + + return null; + } + } + + return acc; + }, + + /** + * Return the DOM node by identifier (may be accessible, DOM node or ID). + */ + getNode(accOrNodeOrID, doc) { + if (!accOrNodeOrID) { + return null; + } + + if (Node.isInstance(accOrNodeOrID)) { + return accOrNodeOrID; + } + + if (accOrNodeOrID instanceof Ci.nsIAccessible) { + return accOrNodeOrID.DOMNode; + } + + const node = doc.getElementById(accOrNodeOrID); + if (!node) { + Assert.ok(false, `Can't get DOM element for ${accOrNodeOrID}`); + return null; + } + + return node; + }, + + /** + * Return root accessible. + * + * @param {DOMNode} doc + * Chrome document. + * + * @return {nsIAccessible} + * Accessible object for chrome window. + */ + getRootAccessible(doc) { + const acc = this.getAccessible(doc); + return acc ? acc.rootDocument.QueryInterface(Ci.nsIAccessible) : null; + }, + + /** + * Analogy of SimpleTest.is function used to compare objects. + */ + isObject(obj, expectedObj, msg) { + if (obj == expectedObj) { + Assert.ok(true, msg); + return; + } + + Assert.ok( + false, + `${msg} - got "${this.prettyName(obj)}", expected "${this.prettyName( + expectedObj + )}"` + ); + }, +}; diff --git a/accessible/tests/browser/Layout.jsm b/accessible/tests/browser/Layout.jsm new file mode 100644 index 0000000000..d51644ec57 --- /dev/null +++ b/accessible/tests/browser/Layout.jsm @@ -0,0 +1,181 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const EXPORTED_SYMBOLS = ["Layout"]; + +const { Assert } = ChromeUtils.import("resource://testing-common/Assert.jsm"); +const { CommonUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm" +); + +const Layout = { + /** + * Zoom the given document. + */ + zoomDocument(doc, zoom) { + const bc = BrowsingContext.getFromWindow(doc.defaultView); + bc.fullZoom = zoom; + }, + + /** + * Set the relative resolution of this document. This is what apz does. + * On non-mobile platforms you won't see a visible change. + */ + setResolution(doc, zoom) { + const windowUtils = doc.defaultView.windowUtils; + windowUtils.setResolutionAndScaleTo(zoom); + }, + + /** + * Assert.is() function checking the expected value is within the range. + */ + isWithin(expected, got, within, msg) { + if (Math.abs(got - expected) <= within) { + Assert.ok(true, `${msg} - Got ${got}`); + } else { + Assert.ok( + false, + `${msg} - Got ${got}, expected ${expected} with error of ${within}` + ); + } + }, + + /** + * Return the accessible coordinates relative to the screen in device pixels. + */ + getPos(id) { + const accessible = CommonUtils.getAccessible(id); + const x = {}; + const y = {}; + accessible.getBounds(x, y, {}, {}); + + return [x.value, y.value]; + }, + + /** + * Return the accessible coordinates and size relative to the screen in device + * pixels. This methods also retrieves coordinates in CSS pixels and ensures that they + * match Dev pixels with a given device pixel ratio. + */ + getBounds(id, dpr) { + const accessible = CommonUtils.getAccessible(id); + const x = {}; + const y = {}; + const width = {}; + const height = {}; + const xInCSS = {}; + const yInCSS = {}; + const widthInCSS = {}; + const heightInCSS = {}; + accessible.getBounds(x, y, width, height); + accessible.getBoundsInCSSPixels(xInCSS, yInCSS, widthInCSS, heightInCSS); + + this.isWithin( + x.value / dpr, + xInCSS.value, + 1, + "X in CSS pixels is calculated correctly" + ); + this.isWithin( + y.value / dpr, + yInCSS.value, + 1, + "Y in CSS pixels is calculated correctly" + ); + this.isWithin( + width.value / dpr, + widthInCSS.value, + 1, + "Width in CSS pixels is calculated correctly" + ); + this.isWithin( + height.value / dpr, + heightInCSS.value, + 1, + "Height in CSS pixels is calculated correctly" + ); + + return [x.value, y.value, width.value, height.value]; + }, + + getRangeExtents(id, startOffset, endOffset, coordOrigin) { + const hyperText = CommonUtils.getAccessible(id, [Ci.nsIAccessibleText]); + const x = {}; + const y = {}; + const width = {}; + const height = {}; + hyperText.getRangeExtents( + startOffset, + endOffset, + x, + y, + width, + height, + coordOrigin + ); + + return [x.value, y.value, width.value, height.value]; + }, + + CSSToDevicePixels(win, x, y, width, height) { + const winUtil = win.windowUtils; + const ratio = winUtil.screenPixelsPerCSSPixel; + + // CSS pixels and ratio can be not integer. Device pixels are always integer. + // Do our best and hope it works. + return [ + Math.round(x * ratio), + Math.round(y * ratio), + Math.round(width * ratio), + Math.round(height * ratio), + ]; + }, + + /** + * Return DOM node coordinates relative the screen and its size in device + * pixels. + */ + getBoundsForDOMElm(id, doc) { + let x = 0; + let y = 0; + let width = 0; + let height = 0; + + const elm = CommonUtils.getNode(id, doc); + const elmWindow = elm.ownerGlobal; + if (elm.localName == "area") { + const mapName = elm.parentNode.getAttribute("name"); + const selector = `[usemap="#${mapName}"]`; + const img = elm.ownerDocument.querySelector(selector); + + const areaCoords = elm.coords.split(","); + const areaX = parseInt(areaCoords[0], 10); + const areaY = parseInt(areaCoords[1], 10); + const areaWidth = parseInt(areaCoords[2], 10) - areaX; + const areaHeight = parseInt(areaCoords[3], 10) - areaY; + + const rect = img.getBoundingClientRect(); + x = rect.left + areaX; + y = rect.top + areaY; + width = areaWidth; + height = areaHeight; + } else { + const rect = elm.getBoundingClientRect(); + x = rect.left; + y = rect.top; + width = rect.width; + height = rect.height; + } + + return this.CSSToDevicePixels( + elmWindow, + x + elmWindow.mozInnerScreenX, + y + elmWindow.mozInnerScreenY, + width, + height + ); + }, +}; diff --git a/accessible/tests/browser/bounds/browser.ini b/accessible/tests/browser/bounds/browser.ini new file mode 100644 index 0000000000..1997e92ff1 --- /dev/null +++ b/accessible/tests/browser/bounds/browser.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + +[browser_test_resolution.js] +skip-if = e10s && os == 'win' # bug 1372296 +[browser_test_zoom.js] +[browser_test_zoom_text.js] +skip-if = e10s && os == 'win' # bug 1372296 diff --git a/accessible/tests/browser/bounds/browser_test_resolution.js b/accessible/tests/browser/bounds/browser_test_resolution.js new file mode 100644 index 0000000000..bdcb3e1792 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_resolution.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/layout.js */ + +async function testScaledBounds(browser, accDoc, scale, id, type = "object") { + let acc = findAccessibleChildByID(accDoc, id); + + // Get document offset + let [docX, docY] = getBounds(accDoc); + + // Get the unscaled bounds of the accessible + let [x, y, width, height] = + type == "text" + ? getRangeExtents(acc, 0, -1, COORDTYPE_SCREEN_RELATIVE) + : getBounds(acc); + + await invokeContentTask(browser, [scale], _scale => { + const { Layout } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + Layout.setResolution(content.document, _scale); + }); + + let [scaledX, scaledY, scaledWidth, scaledHeight] = + type == "text" + ? getRangeExtents(acc, 0, -1, COORDTYPE_SCREEN_RELATIVE) + : getBounds(acc); + + let name = prettyName(acc); + isWithin(scaledWidth, width * scale, 2, "Wrong scaled width of " + name); + isWithin(scaledHeight, height * scale, 2, "Wrong scaled height of " + name); + isWithin(scaledX - docX, (x - docX) * scale, 2, "Wrong scaled x of " + name); + isWithin(scaledY - docY, (y - docY) * scale, 2, "Wrong scaled y of " + name); + + await invokeContentTask(browser, [], () => { + const { Layout } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + Layout.setResolution(content.document, 1.0); + }); +} + +async function runTests(browser, accDoc) { + // The scrollbars get in the way of container bounds calculation. + await SpecialPowers.pushPrefEnv({ + set: [["ui.useOverlayScrollbars", 1]], + }); + + await testScaledBounds(browser, accDoc, 2.0, "p1"); + await testScaledBounds(browser, accDoc, 0.5, "p2"); + await testScaledBounds(browser, accDoc, 3.5, "b1"); + + await testScaledBounds(browser, accDoc, 2.0, "p1", "text"); + await testScaledBounds(browser, accDoc, 0.75, "p2", "text"); +} + +/** + * Test accessible boundaries when page is zoomed + */ +addAccessibleTask( + ` +<p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p> +<p id="p2">para 2</p> +<button id="b1">Hello</button> +`, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_test_zoom.js b/accessible/tests/browser/bounds/browser_test_zoom.js new file mode 100644 index 0000000000..6600a17130 --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_zoom.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/layout.js */ + +async function testContentBounds(browser, acc) { + let [ + expectedX, + expectedY, + expectedWidth, + expectedHeight, + ] = await getContentBoundsForDOMElm(browser, getAccessibleDOMNodeID(acc)); + + let contentDPR = await getContentDPR(browser); + let [x, y, width, height] = getBounds(acc, contentDPR); + let prettyAccName = prettyName(acc); + is(x, expectedX, "Wrong x coordinate of " + prettyAccName); + is(y, expectedY, "Wrong y coordinate of " + prettyAccName); + is(width, expectedWidth, "Wrong width of " + prettyAccName); + ok(height >= expectedHeight, "Wrong height of " + prettyAccName); +} + +async function runTests(browser, accDoc) { + let p1 = findAccessibleChildByID(accDoc, "p1"); + let p2 = findAccessibleChildByID(accDoc, "p2"); + let imgmap = findAccessibleChildByID(accDoc, "imgmap"); + if (!imgmap.childCount) { + // An image map may not be available even after the doc and image load + // is complete. We don't recieve any DOM events for this change either, + // so we need to wait for a REORDER. + await waitForEvent(EVENT_REORDER, "imgmap"); + } + let area = imgmap.firstChild; + + await testContentBounds(browser, p1); + await testContentBounds(browser, p2); + await testContentBounds(browser, area); + + await invokeContentTask(browser, [], () => { + const { Layout } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + Layout.zoomDocument(content.document, 2.0); + }); + + await testContentBounds(browser, p1); + await testContentBounds(browser, p2); + await testContentBounds(browser, area); +} + +/** + * Test accessible boundaries when page is zoomed + */ +addAccessibleTask( + ` +<p id="p1">para 1</p><p id="p2">para 2</p> +<map name="atoz_map" id="map"> + <area id="area1" href="http://mozilla.org" + coords=17,0,30,14" alt="mozilla.org" shape="rect"> +</map> +<img id="imgmap" width="447" height="15" + usemap="#atoz_map" + src="http://example.com/a11y/accessible/tests/mochitest/letters.gif">`, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/browser_test_zoom_text.js b/accessible/tests/browser/bounds/browser_test_zoom_text.js new file mode 100644 index 0000000000..8abbff025d --- /dev/null +++ b/accessible/tests/browser/bounds/browser_test_zoom_text.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/layout.js */ + +async function runTests(browser, accDoc) { + async function testTextNode(id) { + let hyperTextNode = findAccessibleChildByID(accDoc, id); + let textNode = hyperTextNode.firstChild; + + let contentDPR = await getContentDPR(browser); + let [x, y, width, height] = getBounds(textNode, contentDPR); + testTextBounds( + hyperTextNode, + 0, + -1, + [x, y, width, height], + COORDTYPE_SCREEN_RELATIVE + ); + } + + async function testEmptyInputNode(id) { + let inputNode = findAccessibleChildByID(accDoc, id); + + let [x, y, width, height] = getBounds(inputNode); + testTextBounds( + inputNode, + 0, + -1, + [x, y, width, height], + COORDTYPE_SCREEN_RELATIVE + ); + testTextBounds( + inputNode, + 0, + 0, + [x, y, width, height], + COORDTYPE_SCREEN_RELATIVE + ); + } + + await testTextNode("p1"); + await testTextNode("p2"); + await testEmptyInputNode("i1"); + + await invokeContentTask(browser, [], () => { + const { Layout } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + Layout.zoomDocument(content.document, 2.0); + }); + + await testTextNode("p1"); + + await invokeContentTask(browser, [], () => { + const { Layout } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + Layout.zoomDocument(content.document, 1.0); + }); +} + +/** + * Test the text range boundary when page is zoomed + */ +addAccessibleTask( + ` + <p id='p1' style='font-family: monospace;'>Tilimilitryamdiya</p> + <p id='p2'>ل</p> + <form><input id='i1' /></form>`, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/bounds/head.js b/accessible/tests/browser/bounds/head.js new file mode 100644 index 0000000000..f4d20e636c --- /dev/null +++ b/accessible/tests/browser/bounds/head.js @@ -0,0 +1,21 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "layout.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); diff --git a/accessible/tests/browser/browser.ini b/accessible/tests/browser/browser.ini new file mode 100644 index 0000000000..99acf3c0b0 --- /dev/null +++ b/accessible/tests/browser/browser.ini @@ -0,0 +1,34 @@ +[DEFAULT] +skip-if = a11y_checks || (os == 'win' && processor == 'aarch64') # 1534855 +support-files = + !/accessible/tests/mochitest/*.js + *.jsm + head.js + shared-head.js + +[browser_shutdown_acc_reference.js] +skip-if = (os == 'linux' && debug && bits == 64) #Bug 1421307 +[browser_shutdown_doc_acc_reference.js] +[browser_shutdown_multi_acc_reference_obj.js] +[browser_shutdown_multi_acc_reference_doc.js] +[browser_shutdown_multi_reference.js] +[browser_shutdown_parent_own_reference.js] +skip-if = !e10s || (verify && debug && (os == 'win')) # e10s specific test for a11y start/shutdown between parent and content. +[browser_shutdown_pref.js] +[browser_shutdown_proxy_acc_reference.js] +skip-if = !e10s || (os == 'win') # e10s specific test for a11y start/shutdown between parent and content. +[browser_shutdown_proxy_doc_acc_reference.js] +skip-if = !e10s || (os == 'win') || (verify && debug) # e10s specific test for a11y start/shutdown between parent and content. +[browser_shutdown_multi_proxy_acc_reference_doc.js] +skip-if = !e10s || (os == 'win') || (verify && debug && (os == 'linux')) # e10s specific test for a11y start/shutdown between parent and content. +[browser_shutdown_multi_proxy_acc_reference_obj.js] +skip-if = !e10s || (os == 'win') || (verify && debug && (os == 'linux')) # e10s specific test for a11y start/shutdown between parent and content. +[browser_shutdown_remote_no_reference.js] +skip-if = !e10s || (verify && debug && (os == 'win')) # e10s specific test for a11y start/shutdown between parent and content. +[browser_shutdown_remote_only.js] +skip-if = !e10s # e10s specific test for a11y start/shutdown between parent and content. +[browser_shutdown_remote_own_reference.js] +skip-if = !e10s # e10s specific test for a11y start/shutdown between parent and content. +[browser_shutdown_scope_lifecycle.js] +[browser_shutdown_start_restart.js] +skip-if = (verify && debug) diff --git a/accessible/tests/browser/browser_shutdown_acc_reference.js b/accessible/tests/browser/browser_shutdown_acc_reference.js new file mode 100644 index 0000000000..68c07ba2b6 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_acc_reference.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Create a11y service. + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + + await a11yInit; + ok(accService, "Service initialized"); + + // Accessible object reference will live longer than the scope of this + // function. + let acc = await new Promise(resolve => { + let intervalId = setInterval(() => { + let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab); + if (tabAcc) { + clearInterval(intervalId); + resolve(tabAcc); + } + }, 10); + }); + ok(acc, "Accessible object is created"); + + let canShutdown = false; + // This promise will resolve only if canShutdown flag is set to true. If + // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut + // down, the promise will reject. + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + + // Force garbage collection that should not trigger shutdown because there is + // a reference to an accessible object. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a reference to an accessible object. + acc = null; + ok(!acc, "Accessible object is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; +}); diff --git a/accessible/tests/browser/browser_shutdown_doc_acc_reference.js b/accessible/tests/browser/browser_shutdown_doc_acc_reference.js new file mode 100644 index 0000000000..baf2b898e5 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_doc_acc_reference.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Create a11y service. + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + + await a11yInit; + ok(accService, "Service initialized"); + + // Accessible document reference will live longer than the scope of this + // function. + let docAcc = accService.getAccessibleFor(document); + ok(docAcc, "Accessible document is created"); + + let canShutdown = false; + // This promise will resolve only if canShutdown flag is set to true. If + // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut + // down, the promise will reject. + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + + // Force garbage collection that should not trigger shutdown because there is + // a reference to an accessible document. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a reference to an accessible document. + docAcc = null; + ok(!docAcc, "Accessible document is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; +}); diff --git a/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js b/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js new file mode 100644 index 0000000000..b67b2f46f7 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_multi_acc_reference_doc.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Create a11y service. + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + + await a11yInit; + ok(accService, "Service initialized"); + + let docAcc = accService.getAccessibleFor(document); + ok(docAcc, "Accessible document is created"); + + // Accessible object reference will live longer than the scope of this + // function. + let acc = await new Promise(resolve => { + let intervalId = setInterval(() => { + let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab); + if (tabAcc) { + clearInterval(intervalId); + resolve(tabAcc); + } + }, 10); + }); + ok(acc, "Accessible object is created"); + + let canShutdown = false; + // This promise will resolve only if canShutdown flag is set to true. If + // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut + // down, the promise will reject. + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + + // Force garbage collection that should not trigger shutdown because there are + // references to accessible objects. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Remove a reference to an accessible object. + acc = null; + ok(!acc, "Accessible object is removed"); + // Force garbage collection that should not trigger shutdown because there is + // a reference to an accessible document. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a reference to an accessible document. + docAcc = null; + ok(!docAcc, "Accessible document is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; +}); diff --git a/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js b/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js new file mode 100644 index 0000000000..18160a8db7 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_multi_acc_reference_obj.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Create a11y service. + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + + await a11yInit; + ok(accService, "Service initialized"); + + let docAcc = accService.getAccessibleFor(document); + ok(docAcc, "Accessible document is created"); + + // Accessible object reference will live longer than the scope of this + // function. + let acc = await new Promise(resolve => { + let intervalId = setInterval(() => { + let tabAcc = accService.getAccessibleFor(gBrowser.selectedTab); + if (tabAcc) { + clearInterval(intervalId); + resolve(tabAcc); + } + }, 10); + }); + ok(acc, "Accessible object is created"); + + let canShutdown = false; + // This promise will resolve only if canShutdown flag is set to true. If + // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut + // down, the promise will reject. + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + + // Force garbage collection that should not trigger shutdown because there are + // references to accessible objects. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Remove a reference to an accessible document. + docAcc = null; + ok(!docAcc, "Accessible document is removed"); + // Force garbage collection that should not trigger shutdown because there is + // a reference to an accessible object. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a reference to an accessible object. + acc = null; + ok(!acc, "Accessible object is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; +}); diff --git a/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js new file mode 100644 index 0000000000..8763327bae --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_doc.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + let docLoaded = waitForEvent( + Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE, + "body" + ); + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + await a11yInit; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + <html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body id="body"><div id="div"></div></body> + </html>`, + }, + async function(browser) { + let docLoadedEvent = await docLoaded; + let docAcc = docLoadedEvent.accessibleDocument; + ok(docAcc, "Accessible document proxy is created"); + // Remove unnecessary dangling references + docLoaded = null; + docLoadedEvent = null; + forceGC(); + + let acc = docAcc.getChildAt(0); + ok(acc, "Accessible proxy is created"); + + let canShutdown = false; + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there + // is a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Remove a reference to an accessible proxy. + acc = null; + ok(!acc, "Accessible proxy is removed"); + // Force garbage collection that should not trigger shutdown because there is + // a reference to an accessible document proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a last reference to an accessible document proxy. + docAcc = null; + ok(!docAcc, "Accessible document proxy is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js new file mode 100644 index 0000000000..5134901355 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_multi_proxy_acc_reference_obj.js @@ -0,0 +1,90 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + let docLoaded = waitForEvent( + Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE, + "body" + ); + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + await a11yInit; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + <html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body id="body"><div id="div"></div></body> + </html>`, + }, + async function(browser) { + let docLoadedEvent = await docLoaded; + let docAcc = docLoadedEvent.accessibleDocument; + ok(docAcc, "Accessible document proxy is created"); + // Remove unnecessary dangling references + docLoaded = null; + docLoadedEvent = null; + forceGC(); + + let acc = docAcc.getChildAt(0); + ok(acc, "Accessible proxy is created"); + + let canShutdown = false; + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there + // is a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Remove a reference to an accessible document proxy. + docAcc = null; + ok(!docAcc, "Accessible document proxy is removed"); + // Force garbage collection that should not trigger shutdown because there is + // a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a last reference to an accessible proxy. + acc = null; + ok(!acc, "Accessible proxy is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_multi_reference.js b/accessible/tests/browser/browser_shutdown_multi_reference.js new file mode 100644 index 0000000000..cd0bc0d103 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_multi_reference.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + info("Creating a service"); + // Create a11y service. + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService1 = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + ok(accService1, "Service initialized"); + + // Add another reference to a11y service. This will not trigger + // 'a11y-init-or-shutdown' event + let accService2 = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService2, "Service initialized"); + + info("Removing all service references"); + let canShutdown = false; + // This promise will resolve only if canShutdown flag is set to true. If + // 'a11y-init-or-shutdown' event with '0' flag comes before it can be shut + // down, the promise will reject. + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + // Remove first a11y service reference. + accService1 = null; + ok(!accService1, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there is + // another reference. + forceGC(); + + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove last a11y service reference. + accService2 = null; + ok(!accService2, "Service is removed"); + // Force garbage collection that should trigger shutdown. + forceGC(); + await a11yShutdown; +}); diff --git a/accessible/tests/browser/browser_shutdown_parent_own_reference.js b/accessible/tests/browser/browser_shutdown_parent_own_reference.js new file mode 100644 index 0000000000..86115fdaad --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_parent_own_reference.js @@ -0,0 +1,104 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + <html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body></body> + </html>`, + }, + async function(browser) { + info( + "Creating a service in parent and waiting for service to be created " + + "in content" + ); + await loadContentScripts(browser, "Common.jsm"); + // Create a11y service in the main process. This will trigger creating of + // the a11y service in parent as well. + const [parentA11yInitObserver, parentA11yInit] = initAccService(); + const [contentA11yInitObserver, contentA11yInit] = initAccService( + browser + ); + + await Promise.all([parentA11yInitObserver, contentA11yInitObserver]); + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized in parent"); + await Promise.all([parentA11yInit, contentA11yInit]); + + info( + "Adding additional reference to accessibility service in content " + + "process" + ); + // Add a new reference to the a11y service inside the content process. + await SpecialPowers.spawn(browser, [], () => { + content.CommonUtils.accService; + }); + + info( + "Trying to shut down a service in content and making sure it stays " + + "alive as it was started by parent" + ); + let contentCanShutdown = false; + // This promise will resolve only if contentCanShutdown flag is set to true. + // If 'a11y-init-or-shutdown' event with '0' flag (in content) comes before + // it can be shut down, the promise will reject. + const [ + contentA11yShutdownObserver, + contentA11yShutdownPromise, + ] = shutdownAccService(browser); + await contentA11yShutdownObserver; + const contentA11yShutdown = new Promise((resolve, reject) => + contentA11yShutdownPromise.then(flag => + contentCanShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + // Remove a11y service reference in content and force garbage collection. + // This should not trigger shutdown since a11y was originally initialized by + // the main process. + await SpecialPowers.spawn(browser, [], () => { + content.CommonUtils.clearAccService(); + }); + + // Have some breathing room between a11y service shutdowns. + await TestUtils.waitForTick(); + + info("Removing a service in parent"); + // Now allow a11y service to shutdown in content. + contentCanShutdown = true; + // Remove the a11y service reference in the main process. + const [ + parentA11yShutdownObserver, + parentA11yShutdown, + ] = shutdownAccService(); + await parentA11yShutdownObserver; + + accService = null; + ok(!accService, "Service is removed in parent"); + // Force garbage collection that should trigger shutdown in both parent and + // content. + forceGC(); + await Promise.all([parentA11yShutdown, contentA11yShutdown]); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); + } + ); +}); diff --git a/accessible/tests/browser/browser_shutdown_pref.js b/accessible/tests/browser/browser_shutdown_pref.js new file mode 100644 index 0000000000..74cef28b03 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_pref.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const PREF_ACCESSIBILITY_FORCE_DISABLED = "accessibility.force_disabled"; + +add_task(async function testForceDisable() { + ok( + !Services.appinfo.accessibilityEnabled, + "Accessibility is disabled by default" + ); + + info("Reset force disabled preference"); + Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED); + + info("Enable accessibility service via XPCOM"); + let [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + ok(Services.appinfo.accessibilityEnabled, "Accessibility is enabled"); + + info("Force disable a11y service via preference"); + let [a11yShutdownObserver, a11yShutdown] = shutdownAccService(); + await a11yShutdownObserver; + + Services.prefs.setIntPref(PREF_ACCESSIBILITY_FORCE_DISABLED, 1); + await a11yShutdown; + ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled"); + + info("Attempt to get an instance of a11y service and call its method."); + accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + try { + accService.getAccesssibleFor(document); + ok(false, "getAccesssibleFor should've triggered an exception."); + } catch (e) { + ok( + true, + "getAccesssibleFor triggers an exception as a11y service is shutdown." + ); + } + ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled"); + + info("Reset force disabled preference"); + Services.prefs.clearUserPref(PREF_ACCESSIBILITY_FORCE_DISABLED); + + info("Create a11y service again"); + [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + ok(Services.appinfo.accessibilityEnabled, "Accessibility is enabled"); + + info("Remove all references to a11y service"); + [a11yShutdownObserver, a11yShutdown] = shutdownAccService(); + await a11yShutdownObserver; + + accService = null; + forceGC(); + await a11yShutdown; + ok(!Services.appinfo.accessibilityEnabled, "Accessibility is disabled"); +}); diff --git a/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js new file mode 100644 index 0000000000..d6fa715cf3 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_proxy_acc_reference.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + await a11yInit; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + <html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body><div id="div" style="visibility: hidden;"></div></body> + </html>`, + }, + async function(browser) { + let onShow = waitForEvent(Ci.nsIAccessibleEvent.EVENT_SHOW, "div"); + await invokeSetStyle(browser, "div", "visibility", "visible"); + let showEvent = await onShow; + let divAcc = showEvent.accessible; + ok(divAcc, "Accessible proxy is created"); + // Remove unnecessary dangling references + onShow = null; + showEvent = null; + forceGC(); + + let canShutdown = false; + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there + // is a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a last reference to an accessible proxy. + divAcc = null; + ok(!divAcc, "Accessible proxy is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js new file mode 100644 index 0000000000..1dc2344acb --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_proxy_doc_acc_reference.js @@ -0,0 +1,78 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + let docLoaded = waitForEvent( + Ci.nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE, + "body" + ); + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + await a11yInit; + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + <html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body id="body"></body> + </html>`, + }, + async function(browser) { + let docLoadedEvent = await docLoaded; + let docAcc = docLoadedEvent.accessibleDocument; + ok(docAcc, "Accessible document proxy is created"); + // Remove unnecessary dangling references + docLoaded = null; + docLoadedEvent = null; + forceGC(); + + let canShutdown = false; + const [a11yShutdownObserver, a11yShutdownPromise] = shutdownAccService(); + await a11yShutdownObserver; + const a11yShutdown = new Promise((resolve, reject) => + a11yShutdownPromise.then(flag => + canShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should not trigger shutdown because there + // is a reference to an accessible proxy. + forceGC(); + // Have some breathing room when removing a11y service references. + await TestUtils.waitForTick(); + + // Now allow a11y service to shutdown. + canShutdown = true; + // Remove a last reference to an accessible document proxy. + docAcc = null; + ok(!docAcc, "Accessible document proxy is removed"); + + // Force garbage collection that should now trigger shutdown. + forceGC(); + await a11yShutdown; + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_remote_no_reference.js b/accessible/tests/browser/browser_shutdown_remote_no_reference.js new file mode 100644 index 0000000000..cce695579d --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_remote_no_reference.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + <html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body></body> + </html>`, + }, + async function(browser) { + info( + "Creating a service in parent and waiting for service to be created " + + "in content" + ); + await loadContentScripts(browser, "Common.jsm"); + // Create a11y service in the main process. This will trigger creating of + // the a11y service in parent as well. + const [parentA11yInitObserver, parentA11yInit] = initAccService(); + const [contentA11yInitObserver, contentA11yInit] = initAccService( + browser + ); + let [ + parentConsumersChangedObserver, + parentConsumersChanged, + ] = accConsumersChanged(); + let [ + contentConsumersChangedObserver, + contentConsumersChanged, + ] = accConsumersChanged(browser); + + await Promise.all([ + parentA11yInitObserver, + contentA11yInitObserver, + parentConsumersChangedObserver, + contentConsumersChangedObserver, + ]); + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized in parent"); + await Promise.all([parentA11yInit, contentA11yInit]); + await parentConsumersChanged.then(data => + Assert.deepEqual( + data, + { + XPCOM: true, + MainProcess: false, + PlatformAPI: false, + }, + "Accessibility service consumers in parent are correct." + ) + ); + await contentConsumersChanged.then(data => + Assert.deepEqual( + data, + { + XPCOM: false, + MainProcess: true, + PlatformAPI: false, + }, + "Accessibility service consumers in content are correct." + ) + ); + + Assert.deepEqual( + JSON.parse(accService.getConsumers()), + { + XPCOM: true, + MainProcess: false, + PlatformAPI: false, + }, + "Accessibility service consumers in parent are correct." + ); + + info( + "Removing a service in parent and waiting for service to be shut " + + "down in content" + ); + // Remove a11y service reference in the main process. + const [ + parentA11yShutdownObserver, + parentA11yShutdown, + ] = shutdownAccService(); + const [ + contentA11yShutdownObserver, + contentA11yShutdown, + ] = shutdownAccService(browser); + [ + parentConsumersChangedObserver, + parentConsumersChanged, + ] = accConsumersChanged(); + [ + contentConsumersChangedObserver, + contentConsumersChanged, + ] = accConsumersChanged(browser); + + await Promise.all([ + parentA11yShutdownObserver, + contentA11yShutdownObserver, + parentConsumersChangedObserver, + contentConsumersChangedObserver, + ]); + + accService = null; + ok(!accService, "Service is removed in parent"); + // Force garbage collection that should trigger shutdown in both main and + // content process. + forceGC(); + await Promise.all([parentA11yShutdown, contentA11yShutdown]); + await parentConsumersChanged.then(data => + Assert.deepEqual( + data, + { + XPCOM: false, + MainProcess: false, + PlatformAPI: false, + }, + "Accessibility service consumers are correct." + ) + ); + await contentConsumersChanged.then(data => + Assert.deepEqual( + data, + { + XPCOM: false, + MainProcess: false, + PlatformAPI: false, + }, + "Accessibility service consumers are correct." + ) + ); + } + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); +}); diff --git a/accessible/tests/browser/browser_shutdown_remote_only.js b/accessible/tests/browser/browser_shutdown_remote_only.js new file mode 100644 index 0000000000..8798170c05 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_remote_only.js @@ -0,0 +1,56 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + <html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body></body> + </html>`, + }, + async function(browser) { + info("Creating a service in content"); + await loadContentScripts(browser, "Common.jsm"); + // Create a11y service in the content process. + const [a11yInitObserver, a11yInit] = initAccService(browser); + await a11yInitObserver; + await SpecialPowers.spawn(browser, [], () => { + content.CommonUtils.accService; + }); + await a11yInit; + ok( + true, + "Accessibility service is started in content process correctly." + ); + + info("Removing a service in content"); + // Remove a11y service reference from the content process. + const [a11yShutdownObserver, a11yShutdown] = shutdownAccService(browser); + await a11yShutdownObserver; + // Force garbage collection that should trigger shutdown. + await SpecialPowers.spawn(browser, [], () => { + content.CommonUtils.clearAccService(); + }); + await a11yShutdown; + ok( + true, + "Accessibility service is shutdown in content process correctly." + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); + } + ); +}); diff --git a/accessible/tests/browser/browser_shutdown_remote_own_reference.js b/accessible/tests/browser/browser_shutdown_remote_own_reference.js new file mode 100644 index 0000000000..2d88e5ed85 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_remote_own_reference.js @@ -0,0 +1,191 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Making sure that the e10s is enabled on Windows for testing. + await setE10sPrefs(); + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: `data:text/html, + <html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body></body> + </html>`, + }, + async function(browser) { + info( + "Creating a service in parent and waiting for service to be created " + + "in content" + ); + await loadContentScripts(browser, "Common.jsm"); + // Create a11y service in the main process. This will trigger creating of + // the a11y service in parent as well. + const [parentA11yInitObserver, parentA11yInit] = initAccService(); + const [contentA11yInitObserver, contentA11yInit] = initAccService( + browser + ); + let [ + contentConsumersChangedObserver, + contentConsumersChanged, + ] = accConsumersChanged(browser); + + await Promise.all([ + parentA11yInitObserver, + contentA11yInitObserver, + contentConsumersChangedObserver, + ]); + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized in parent"); + await Promise.all([parentA11yInit, contentA11yInit]); + await contentConsumersChanged.then(data => + Assert.deepEqual( + data, + { + XPCOM: false, + MainProcess: true, + PlatformAPI: false, + }, + "Accessibility service consumers in content are correct." + ) + ); + + info( + "Adding additional reference to accessibility service in content " + + "process" + ); + [ + contentConsumersChangedObserver, + contentConsumersChanged, + ] = accConsumersChanged(browser); + await contentConsumersChangedObserver; + // Add a new reference to the a11y service inside the content process. + await SpecialPowers.spawn(browser, [], () => { + content.CommonUtils.accService; + }); + await contentConsumersChanged.then(data => + Assert.deepEqual( + data, + { + XPCOM: true, + MainProcess: true, + PlatformAPI: false, + }, + "Accessibility service consumers in content are correct." + ) + ); + + const contentConsumers = await SpecialPowers.spawn(browser, [], () => + content.CommonUtils.accService.getConsumers() + ); + Assert.deepEqual( + JSON.parse(contentConsumers), + { + XPCOM: true, + MainProcess: true, + PlatformAPI: false, + }, + "Accessibility service consumers in parent are correct." + ); + + info( + "Shutting down a service in parent and making sure the one in " + + "content stays alive" + ); + let contentCanShutdown = false; + const [ + parentA11yShutdownObserver, + parentA11yShutdown, + ] = shutdownAccService(); + [ + contentConsumersChangedObserver, + contentConsumersChanged, + ] = accConsumersChanged(browser); + // This promise will resolve only if contentCanShutdown flag is set to true. + // If 'a11y-init-or-shutdown' event with '0' flag (in content) comes before + // it can be shut down, the promise will reject. + const [ + contentA11yShutdownObserver, + contentA11yShutdownPromise, + ] = shutdownAccService(browser); + const contentA11yShutdown = new Promise((resolve, reject) => + contentA11yShutdownPromise.then(flag => + contentCanShutdown + ? resolve() + : reject("Accessible service was shut down incorrectly") + ) + ); + + await Promise.all([ + parentA11yShutdownObserver, + contentA11yShutdownObserver, + contentConsumersChangedObserver, + ]); + // Remove a11y service reference in the main process and force garbage + // collection. This should not trigger shutdown in content since a11y + // service is used by XPCOM. + accService = null; + ok(!accService, "Service is removed in parent"); + // Force garbage collection that should not trigger shutdown because there + // is a reference in a content process. + forceGC(); + await SpecialPowers.spawn(browser, [], () => { + SpecialPowers.Cu.forceGC(); + }); + await parentA11yShutdown; + await contentConsumersChanged.then(data => + Assert.deepEqual( + data, + { + XPCOM: true, + MainProcess: false, + PlatformAPI: false, + }, + "Accessibility service consumers in content are correct." + ) + ); + + // Have some breathing room between a11y service shutdowns. + await TestUtils.waitForTick(); + + info("Removing a service in content"); + // Now allow a11y service to shutdown in content. + contentCanShutdown = true; + [ + contentConsumersChangedObserver, + contentConsumersChanged, + ] = accConsumersChanged(browser); + await contentConsumersChangedObserver; + // Remove last reference to a11y service in content and force garbage + // collection that should trigger shutdown. + await SpecialPowers.spawn(browser, [], () => { + content.CommonUtils.clearAccService(); + }); + await contentA11yShutdown; + await contentConsumersChanged.then(data => + Assert.deepEqual( + data, + { + XPCOM: false, + MainProcess: false, + PlatformAPI: false, + }, + "Accessibility service consumers in content are correct." + ) + ); + + // Unsetting e10s related preferences. + await unsetE10sPrefs(); + } + ); +}); diff --git a/accessible/tests/browser/browser_shutdown_scope_lifecycle.js b/accessible/tests/browser/browser_shutdown_scope_lifecycle.js new file mode 100644 index 0000000000..b4dad44de8 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_scope_lifecycle.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + // Create a11y service inside of the function scope. Its reference should be + // released once the anonimous function is called. + const [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + const a11yInitThenShutdown = a11yInit.then(async () => { + const [a11yShutdownObserver, a11yShutdown] = shutdownAccService(); + await a11yShutdownObserver; + return a11yShutdown; + }); + + (function() { + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + ok(accService, "Service initialized"); + })(); + + // Force garbage collection that should trigger shutdown. + forceGC(); + await a11yInitThenShutdown; +}); diff --git a/accessible/tests/browser/browser_shutdown_start_restart.js b/accessible/tests/browser/browser_shutdown_start_restart.js new file mode 100644 index 0000000000..bac7a61da7 --- /dev/null +++ b/accessible/tests/browser/browser_shutdown_start_restart.js @@ -0,0 +1,51 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +add_task(async function() { + info("Creating a service"); + // Create a11y service. + let [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + ok(accService, "Service initialized"); + + info("Removing a service"); + // Remove the only reference to an a11y service. + let [a11yShutdownObserver, a11yShutdown] = shutdownAccService(); + await a11yShutdownObserver; + + accService = null; + ok(!accService, "Service is removed"); + // Force garbage collection that should trigger shutdown. + forceGC(); + await a11yShutdown; + + info("Recreating a service"); + // Re-create a11y service. + [a11yInitObserver, a11yInit] = initAccService(); + await a11yInitObserver; + + accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + await a11yInit; + ok(accService, "Service initialized again"); + + info("Removing a service again"); + // Remove the only reference to an a11y service again. + [a11yShutdownObserver, a11yShutdown] = shutdownAccService(); + await a11yShutdownObserver; + + accService = null; + ok(!accService, "Service is removed again"); + // Force garbage collection that should trigger shutdown. + forceGC(); + await a11yShutdown; +}); diff --git a/accessible/tests/browser/e10s/browser.ini b/accessible/tests/browser/e10s/browser.ini new file mode 100644 index 0000000000..a582b9b8bb --- /dev/null +++ b/accessible/tests/browser/e10s/browser.ini @@ -0,0 +1,60 @@ +[DEFAULT] +support-files = + head.js + doc_treeupdate_ariadialog.html + doc_treeupdate_ariaowns.html + doc_treeupdate_imagemap.html + doc_treeupdate_removal.xhtml + doc_treeupdate_visibility.html + doc_treeupdate_whitespace.html + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + !/accessible/tests/mochitest/moz.png + +# Caching tests +[browser_caching_attributes.js] +[browser_caching_description.js] +[browser_caching_name.js] +skip-if = (os == "linux" && bits == 64) || (debug && os == "mac") || (debug && os == "win") #Bug 1388256 +[browser_caching_relations.js] +[browser_caching_states.js] +[browser_caching_value.js] +[browser_caching_uniqueid.js] + +# Events tests +[browser_events_announcement.js] +skip-if = e10s && os == 'win' # Bug 1288839 +[browser_events_caretmove.js] +[browser_events_hide.js] +[browser_events_show.js] +[browser_events_statechange.js] +[browser_events_textchange.js] +[browser_events_vcchange.js] + +# Text tests +[browser_text_paragraph_boundary.js] + +# Tree update tests +[browser_treeupdate_ariadialog.js] +[browser_treeupdate_ariaowns.js] +[browser_treeupdate_canvas.js] +skip-if = (os == 'win' && os_version == '10.0' && bits == 64 && !debug) #Bug 1462638 - Disabled on Win10 opt/pgo for frequent failures +[browser_treeupdate_cssoverflow.js] +[browser_treeupdate_doc.js] +skip-if = e10s && os == 'win' # Bug 1288839 +[browser_treeupdate_gencontent.js] +[browser_treeupdate_hidden.js] +[browser_treeupdate_imagemap.js] +[browser_treeupdate_list.js] +[browser_treeupdate_list_editabledoc.js] +[browser_treeupdate_listener.js] +[browser_treeupdate_optgroup.js] +[browser_treeupdate_removal.js] +[browser_treeupdate_select_dropdown.js] +[browser_treeupdate_table.js] +[browser_treeupdate_textleaf.js] +[browser_treeupdate_visibility.js] +[browser_treeupdate_whitespace.js] +skip-if = true # Failing due to incorrect index of test container children on document load. diff --git a/accessible/tests/browser/e10s/browser_caching_attributes.js b/accessible/tests/browser/e10s/browser_caching_attributes.js new file mode 100644 index 0000000000..b8e157b068 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_attributes.js @@ -0,0 +1,134 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/attributes.js */ +loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); + +/** + * Default textbox accessible attributes. + */ +const defaultAttributes = { + "margin-top": "0px", + "margin-right": "0px", + "margin-bottom": "0px", + "margin-left": "0px", + "text-align": "start", + "text-indent": "0px", + id: "textbox", + tag: "input", + display: "inline-block", +}; + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * expected {Object} expected attributes for given accessibles + * unexpected {Object} unexpected attributes for given accessibles + * + * action {?AsyncFunction} an optional action that awaits a change in + * attributes + * attrs {?Array} an optional list of attributes to update + * waitFor {?Number} an optional event to wait for + * } + */ +const attributesTests = [ + { + desc: "Initiall accessible attributes", + expected: defaultAttributes, + unexpected: { + "line-number": "1", + "explicit-name": "true", + "container-live": "polite", + live: "polite", + }, + }, + { + desc: "@line-number attribute is present when textbox is focused", + async action(browser) { + await invokeFocus(browser, "textbox"); + }, + waitFor: EVENT_FOCUS, + expected: Object.assign({}, defaultAttributes, { "line-number": "1" }), + unexpected: { + "explicit-name": "true", + "container-live": "polite", + live: "polite", + }, + }, + { + desc: "@aria-live sets container-live and live attributes", + attrs: [ + { + attr: "aria-live", + value: "polite", + }, + ], + expected: Object.assign({}, defaultAttributes, { + "line-number": "1", + "container-live": "polite", + live: "polite", + }), + unexpected: { + "explicit-name": "true", + }, + }, + { + desc: "@title attribute sets explicit-name attribute to true", + attrs: [ + { + attr: "title", + value: "textbox", + }, + ], + expected: Object.assign({}, defaultAttributes, { + "line-number": "1", + "explicit-name": "true", + "container-live": "polite", + live: "polite", + }), + unexpected: {}, + }, +]; + +/** + * Test caching of accessible object attributes + */ +addAccessibleTask( + ` + <input id="textbox" value="hello">`, + async function(browser, accDoc) { + let textbox = findAccessibleChildByID(accDoc, "textbox"); + for (let { + desc, + action, + attrs, + expected, + waitFor, + unexpected, + } of attributesTests) { + info(desc); + let onUpdate; + + if (waitFor) { + onUpdate = waitForEvent(waitFor, "textbox"); + } + + if (action) { + await action(browser); + } else if (attrs) { + for (let { attr, value } of attrs) { + await invokeSetAttribute(browser, "textbox", attr, value); + } + } + + await onUpdate; + testAttrs(textbox, expected); + testAbsentAttrs(textbox, unexpected); + } + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_description.js b/accessible/tests/browser/e10s/browser_caching_description.js new file mode 100644 index 0000000000..5b9e70ce07 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_description.js @@ -0,0 +1,214 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/name.js */ +loadScripts({ name: "name.js", dir: MOCHITESTS_DIR }); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * expected {String} expected description value for a given accessible + * attrs {?Array} an optional list of attributes to update + * waitFor {?Array} an optional list of accessible events to wait for when + * attributes are updated + * } + */ +const tests = [ + { + desc: "No description when there are no @alt, @title and @aria-describedby", + expected: "", + }, + { + desc: "Description from @aria-describedby attribute", + attrs: [ + { + attr: "aria-describedby", + value: "description", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "aria description", + }, + { + desc: + "No description from @aria-describedby since it is the same as the " + + "@alt attribute which is used as the name", + attrs: [ + { + attr: "alt", + value: "aria description", + }, + ], + waitFor: [[EVENT_REORDER, matchContentDoc]], + expected: "", + }, + { + desc: + "Description from @aria-describedby attribute when @alt and " + + "@aria-describedby are not the same", + attrs: [ + { + attr: "aria-describedby", + value: "description2", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "another description", + }, + { + desc: + "Description from @aria-describedby attribute when @title (used for " + + "name) and @aria-describedby are not the same", + attrs: [ + { + attr: "alt", + }, + { + attr: "title", + value: "title", + }, + ], + waitFor: [[EVENT_REORDER, matchContentDoc]], + expected: "another description", + }, + { + desc: + "No description from @aria-describedby since it is the same as the " + + "@title attribute which is used as the name", + attrs: [ + { + attr: "title", + value: "another description", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "", + }, + { + desc: "No description with only @title attribute which is used as the name", + attrs: [ + { + attr: "aria-describedby", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "", + }, + { + desc: + "Description from @title attribute when @alt and @atitle are not the " + + "same", + attrs: [ + { + attr: "alt", + value: "aria description", + }, + ], + waitFor: [[EVENT_REORDER, matchContentDoc]], + expected: "another description", + }, + { + desc: + "No description from @title since it is the same as the @alt " + + "attribute which is used as the name", + attrs: [ + { + attr: "alt", + value: "another description", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "", + }, + { + desc: + "No description from @aria-describedby since it is the same as the " + + "@alt (used for name) and @title attributes", + attrs: [ + { + attr: "aria-describedby", + value: "description2", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "", + }, + { + desc: + "Description from @aria-describedby attribute when it is different " + + "from @alt (used for name) and @title attributes", + attrs: [ + { + attr: "aria-describedby", + value: "description", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "aria description", + }, + { + desc: + "No description from @aria-describedby since it is the same as the " + + "@alt attribute (used for name) but different from title", + attrs: [ + { + attr: "alt", + value: "aria description", + }, + ], + waitFor: [[EVENT_NAME_CHANGE, "image"]], + expected: "", + }, + { + desc: + "Description from @aria-describedby attribute when @alt (used for " + + "name) and @aria-describedby are not the same but @title and " + + "aria-describedby are", + attrs: [ + { + attr: "aria-describedby", + value: "description2", + }, + ], + waitFor: [[EVENT_DESCRIPTION_CHANGE, "image"]], + expected: "another description", + }, +]; + +/** + * Test caching of accessible object description + */ +addAccessibleTask( + ` + <p id="description">aria description</p> + <p id="description2">another description</p> + <img id="image" />`, + async function(browser, accDoc) { + let imgAcc = findAccessibleChildByID(accDoc, "image"); + + for (let { desc, waitFor, attrs, expected } of tests) { + info(desc); + let onUpdate; + if (waitFor) { + onUpdate = waitForOrderedEvents(waitFor); + } + if (attrs) { + for (let { attr, value } of attrs) { + await invokeSetAttribute(browser, "image", attr, value); + } + } + await onUpdate; + // When attribute change (alt) triggers reorder event, accessible will + // become defunct. + if (isDefunct(imgAcc)) { + imgAcc = findAccessibleChildByID(accDoc, "image"); + } + testDescr(imgAcc, expected); + } + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_name.js b/accessible/tests/browser/e10s/browser_caching_name.js new file mode 100644 index 0000000000..cac05a04de --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_name.js @@ -0,0 +1,539 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/name.js */ +loadScripts({ name: "name.js", dir: MOCHITESTS_DIR }); + +/** + * Rules for name tests that are inspired by + * accessible/tests/mochitest/name/markuprules.xul + * + * Each element in the list of rules represents a name calculation rule for a + * particular test case. + * + * The rules have the following format: + * { attr } - calculated from attribute + * { elm } - calculated from another element + * { fromsubtree } - calculated from element's subtree + * + * + * Options include: + * * waitFor - changes in the subtree will result in an accessible event + * being fired, the test must only continue after the event + * is receieved. + */ +const ARIARule = [ + { attr: "aria-labelledby" }, + { attr: "aria-label", waitFor: EVENT_NAME_CHANGE }, +]; +const HTMLControlHeadRule = [...ARIARule, { elm: "label", isSibling: true }]; +const rules = { + CSSContent: [{ elm: "style", isSibling: true }, { fromsubtree: true }], + HTMLARIAGridCell: [...ARIARule, { fromsubtree: true }, { attr: "title" }], + HTMLControl: [ + ...HTMLControlHeadRule, + { fromsubtree: true }, + { attr: "title" }, + ], + HTMLElm: [...ARIARule, { attr: "title" }], + HTMLImg: [ + ...ARIARule, + { attr: "alt", waitFor: EVENT_NAME_CHANGE }, + { attr: "title" }, + ], + HTMLImgEmptyAlt: [...ARIARule, { attr: "title" }, { attr: "alt" }], + HTMLInputButton: [ + ...HTMLControlHeadRule, + { attr: "value" }, + { attr: "title" }, + ], + HTMLInputImage: [ + ...HTMLControlHeadRule, + { attr: "alt", waitFor: EVENT_NAME_CHANGE }, + { attr: "value" }, + { attr: "title" }, + ], + HTMLInputImageNoValidSrc: [ + ...HTMLControlHeadRule, + { attr: "alt", waitFor: EVENT_NAME_CHANGE }, + { attr: "value" }, + ], + HTMLInputReset: [ + ...HTMLControlHeadRule, + { attr: "value", waitFor: EVENT_TEXT_INSERTED }, + ], + HTMLInputSubmit: [ + ...HTMLControlHeadRule, + { attr: "value", waitFor: EVENT_TEXT_INSERTED }, + ], + HTMLLink: [...ARIARule, { fromsubtree: true }, { attr: "title" }], + HTMLLinkImage: [...ARIARule, { elm: "img" }, { attr: "title" }], + HTMLOption: [ + ...ARIARule, + { attr: "label" }, + { fromsubtree: true }, + { attr: "title" }, + ], + HTMLTable: [ + ...ARIARule, + { elm: "caption" }, + { attr: "summary" }, + { attr: "title" }, + ], +}; + +const markupTests = [ + { + id: "btn", + ruleset: "HTMLControl", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn">test4</label> + <button id="btn" + aria-label="test1" + aria-labelledby="l1 l2" + title="test5">press me</button>`, + expected: ["test2 test3", "test1", "test4", "press me", "test5"], + }, + { + id: "btn", + ruleset: "HTMLInputButton", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn">test4</label> + <input id="btn" + type="button" + aria-label="test1" + aria-labelledby="l1 l2" + value="name from value" + alt="no name from al" + src="no name from src" + data="no name from data" + title="name from title"/>`, + expected: [ + "test2 test3", + "test1", + "test4", + "name from value", + "name from title", + ], + }, + { + id: "btn-submit", + ruleset: "HTMLInputSubmit", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn-submit">test4</label> + <input id="btn-submit" + type="submit" + aria-label="test1" + aria-labelledby="l1 l2" + value="name from value" + alt="no name from atl" + src="no name from src" + data="no name from data" + title="no name from title"/>`, + expected: ["test2 test3", "test1", "test4", "name from value"], + }, + { + id: "btn-reset", + ruleset: "HTMLInputReset", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn-reset">test4</label> + <input id="btn-reset" + type="reset" + aria-label="test1" + aria-labelledby="l1 l2" + value="name from value" + alt="no name from alt" + src="no name from src" + data="no name from data" + title="no name from title"/>`, + expected: ["test2 test3", "test1", "test4", "name from value"], + }, + { + id: "btn-image", + ruleset: "HTMLInputImage", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn-image">test4</label> + <input id="btn-image" + type="image" + aria-label="test1" + aria-labelledby="l1 l2" + alt="name from alt" + value="name from value" + src="http://example.com/a11y/accessible/tests/mochitest/moz.png" + data="no name from data" + title="name from title"/>`, + expected: [ + "test2 test3", + "test1", + "test4", + "name from alt", + "name from value", + "name from title", + ], + }, + { + id: "btn-image", + ruleset: "HTMLInputImageNoValidSrc", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="btn-image">test4</label> + <input id="btn-image" + type="image" + aria-label="test1" + aria-labelledby="l1 l2" + alt="name from alt" + value="name from value" + data="no name from data" + title="no name from title"/>`, + expected: [ + "test2 test3", + "test1", + "test4", + "name from alt", + "name from value", + ], + }, + { + id: "opt", + ruleset: "HTMLOption", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <select> + <option id="opt" + aria-label="test1" + aria-labelledby="l1 l2" + label="test4" + title="test5">option1</option> + <option>option2</option> + </select>`, + expected: ["test2 test3", "test1", "test4", "option1", "test5"], + }, + { + id: "img", + ruleset: "HTMLImg", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <img id="img" + aria-label="Logo of Mozilla" + aria-labelledby="l1 l2" + alt="Mozilla logo" + title="This is a logo" + src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/>`, + expected: [ + "test2 test3", + "Logo of Mozilla", + "Mozilla logo", + "This is a logo", + ], + }, + { + id: "imgemptyalt", + ruleset: "HTMLImgEmptyAlt", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <img id="imgemptyalt" + aria-label="Logo of Mozilla" + aria-labelledby="l1 l2" + title="This is a logo" + alt="" + src="http://example.com/a11y/accessible/tests/mochitest/moz.png"/>`, + expected: ["test2 test3", "Logo of Mozilla", "This is a logo", ""], + }, + { + id: "tc", + ruleset: "HTMLElm", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="tc">test4</label> + <table> + <tr> + <td id="tc" + aria-label="test1" + aria-labelledby="l1 l2" + title="test5"> + <p>This is a paragraph</p> + <a href="#">This is a link</a> + <ul> + <li>This is a list</li> + </ul> + </td> + </tr> + </table>`, + expected: ["test2 test3", "test1", "test5"], + }, + { + id: "gc", + ruleset: "HTMLARIAGridCell", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <label for="gc">test4</label> + <table> + <tr> + <td id="gc" + role="gridcell" + aria-label="test1" + aria-labelledby="l1 l2" + title="This is a paragraph This is a link This is a list"> + <p>This is a paragraph</p> + <a href="#">This is a link</a> + <ul> + <li>Listitem1</li> + <li>Listitem2</li> + </ul> + </td> + </tr> + </table>`, + expected: [ + "test2 test3", + "test1", + "This is a paragraph This is a link \u2022 Listitem1 \u2022 Listitem2", + "This is a paragraph This is a link This is a list", + ], + }, + { + id: "t", + ruleset: "HTMLTable", + markup: ` + <span id="l1">lby_tst6_1</span> + <span id="l2">lby_tst6_2</span> + <label for="t">label_tst6</label> + <table id="t" + aria-label="arialabel_tst6" + aria-labelledby="l1 l2" + summary="summary_tst6" + title="title_tst6"> + <caption>caption_tst6</caption> + <tr> + <td>cell1</td> + <td>cell2</td> + </tr> + </table>`, + expected: [ + "lby_tst6_1 lby_tst6_2", + "arialabel_tst6", + "caption_tst6", + "summary_tst6", + "title_tst6", + ], + }, + { + id: "btn", + ruleset: "CSSContent", + markup: ` + <div role="main"> + <style> + button::before { + content: "do not "; + } + </style> + <button id="btn">press me</button> + </div>`, + expected: ["do not press me", "press me"], + }, + { + // TODO: uncomment when Bug-1256382 is resoved. + // id: 'li', + // ruleset: 'CSSContent', + // markup: ` + // <style> + // ul { + // list-style-type: decimal; + // } + // </style> + // <ul id="ul"> + // <li id="li">Listitem</li> + // </ul>`, + // expected: ['1. Listitem', `${String.fromCharCode(0x2022)} Listitem`] + // }, { + id: "a", + ruleset: "HTMLLink", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <a id="a" + aria-label="test1" + aria-labelledby="l1 l2" + title="test4">test5</a>`, + expected: ["test2 test3", "test1", "test5", "test4"], + }, + { + id: "a-img", + ruleset: "HTMLLinkImage", + markup: ` + <span id="l1">test2</span> + <span id="l2">test3</span> + <a id="a-img" + aria-label="test1" + aria-labelledby="l1 l2" + title="test4"><img alt="test5"/></a>`, + expected: ["test2 test3", "test1", "test5", "test4"], + }, +]; + +/** + * Test accessible name that is calculated from an attribute, remove the + * attribute before proceeding to the next name test. If attribute removal + * results in a reorder or text inserted event - wait for it. If accessible + * becomes defunct, update its reference using the one that is attached to one + * of the above events. + * @param {Object} browser current "tabbrowser" element + * @param {Object} target { acc, parent, id } structure that contains an + * accessible, its parent and its content element + * id. + * @param {Object} rule current attr rule for name calculation + * @param {[type]} expected expected name value + */ +async function testAttrRule(browser, target, rule, expected) { + let { id, parent, acc } = target; + let { waitFor, attr } = rule; + + testName(acc, expected); + + if (waitFor) { + let [event] = await contentSpawnMutation( + browser, + { + expected: [[waitFor, waitFor === EVENT_REORDER ? parent : id]], + }, + (contentId, contentAttr) => + content.document.getElementById(contentId).removeAttribute(contentAttr), + [id, attr] + ); + + // Update accessible just in case it is now defunct. + target.acc = findAccessibleChildByID(event.accessible, id); + } else { + await invokeSetAttribute(browser, id, attr); + } +} + +/** + * Test accessible name that is calculated from an element name, remove the + * element before proceeding to the next name test. If element removal results + * in a reorder event - wait for it. If accessible becomes defunct, update its + * reference using the one that is attached to a possible reorder event. + * @param {Object} browser current "tabbrowser" element + * @param {Object} target { acc, parent, id } structure that contains an + * accessible, its parent and its content element + * id. + * @param {Object} rule current elm rule for name calculation + * @param {[type]} expected expected name value + */ +async function testElmRule(browser, target, rule, expected) { + let { id, parent, acc } = target; + let { isSibling, elm } = rule; + + testName(acc, expected); + let [event] = await contentSpawnMutation( + browser, + { + expected: [[EVENT_REORDER, isSibling ? parent : id]], + }, + contentElm => content.document.querySelector(`${contentElm}`).remove(), + [elm] + ); + + // Update accessible just in case it is now defunct. + target.acc = findAccessibleChildByID(event.accessible, id); +} + +/** + * Test accessible name that is calculated from its subtree, remove the subtree + * and wait for a reorder event before proceeding to the next name test. If + * accessible becomes defunct, update its reference using the one that is + * attached to a reorder event. + * @param {Object} browser current "tabbrowser" element + * @param {Object} target { acc, parent, id } structure that contains an + * accessible, its parent and its content element + * id. + * @param {Object} rule current subtree rule for name calculation + * @param {[type]} expected expected name value + */ +async function testSubtreeRule(browser, target, rule, expected) { + let { id, acc } = target; + + testName(acc, expected); + let [event] = await contentSpawnMutation( + browser, + { + expected: [[EVENT_REORDER, id]], + }, + contentId => { + let elm = content.document.getElementById(contentId); + while (elm.firstChild) { + elm.firstChild.remove(); + } + }, + [id] + ); + + // Update accessible just in case it is now defunct. + target.acc = findAccessibleChildByID(event.accessible, id); +} + +/** + * Iterate over a list of rules and test accessible names for each one of the + * rules. + * @param {Object} browser current "tabbrowser" element + * @param {Object} target { acc, parent, id } structure that contains an + * accessible, its parent and its content element + * id. + * @param {Array} ruleset A list of rules to test a target with + * @param {Array} expected A list of expected name value for each rule + */ +async function testNameRule(browser, target, ruleset, expected) { + for (let i = 0; i < ruleset.length; ++i) { + let rule = ruleset[i]; + let testFn; + if (rule.attr) { + testFn = testAttrRule; + } else if (rule.elm) { + testFn = testElmRule; + } else if (rule.fromsubtree) { + testFn = testSubtreeRule; + } + await testFn(browser, target, rule, expected[i]); + } +} + +markupTests.forEach(({ id, ruleset, markup, expected }) => + addAccessibleTask( + markup, + async function(browser, accDoc) { + const observer = { + observe(subject, topic, data) { + const event = subject.QueryInterface(nsIAccessibleEvent); + console.log(eventToString(event)); + }, + }; + Services.obs.addObserver(observer, "accessible-event"); + // Find a target accessible from an accessible subtree. + let acc = findAccessibleChildByID(accDoc, id); + // Find target's parent accessible from an accessible subtree. + let parent = getAccessibleDOMNodeID(acc.parent); + let target = { id, parent, acc }; + await testNameRule(browser, target, rules[ruleset], expected); + Services.obs.removeObserver(observer, "accessible-event"); + }, + { iframe: true, remoteIframe: true } + ) +); diff --git a/accessible/tests/browser/e10s/browser_caching_relations.js b/accessible/tests/browser/e10s/browser_caching_relations.js new file mode 100644 index 0000000000..38d3a0c63b --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_relations.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/relations.js */ +loadScripts({ name: "relations.js", dir: MOCHITESTS_DIR }); + +/** + * A test specification that has the following format: + * [ + * attr relevant aria attribute + * hostRelation corresponding host relation type + * dependantRelation corresponding dependant relation type + * ] + */ +const attrRelationsSpec = [ + ["aria-labelledby", RELATION_LABELLED_BY, RELATION_LABEL_FOR], + ["aria-describedby", RELATION_DESCRIBED_BY, RELATION_DESCRIPTION_FOR], + ["aria-controls", RELATION_CONTROLLER_FOR, RELATION_CONTROLLED_BY], + ["aria-flowto", RELATION_FLOWS_TO, RELATION_FLOWS_FROM], +]; + +async function testRelated( + browser, + accDoc, + attr, + hostRelation, + dependantRelation +) { + let host = findAccessibleChildByID(accDoc, "host"); + let dependant1 = findAccessibleChildByID(accDoc, "dependant1"); + let dependant2 = findAccessibleChildByID(accDoc, "dependant2"); + + /** + * Test data has the format of: + * { + * desc {String} description for better logging + * attrs {?Array} an optional list of attributes to update + * expected {Array} expected relation values for dependant1, dependant2 + * and host respectively. + * } + */ + const tests = [ + { + desc: "No attribute", + expected: [null, null, null], + }, + { + desc: "Set attribute", + attrs: [{ key: attr, value: "dependant1" }], + expected: [host, null, dependant1], + }, + { + desc: "Change attribute", + attrs: [{ key: attr, value: "dependant2" }], + expected: [null, host, dependant2], + }, + { + desc: "Remove attribute", + attrs: [{ key: attr }], + expected: [null, null, null], + }, + ]; + + for (let { desc, attrs, expected } of tests) { + info(desc); + + if (attrs) { + for (let { key, value } of attrs) { + await invokeSetAttribute(browser, "host", key, value); + } + } + + testRelation(dependant1, dependantRelation, expected[0]); + testRelation(dependant2, dependantRelation, expected[1]); + testRelation(host, hostRelation, expected[2]); + } +} + +/** + * Test caching of relations between accessible objects. + */ +addAccessibleTask( + ` + <div id="dependant1">label</div> + <div id="dependant2">label2</div> + <div role="checkbox" id="host"></div>`, + async function(browser, accDoc) { + for (let spec of attrRelationsSpec) { + await testRelated(browser, accDoc, ...spec); + } + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_states.js b/accessible/tests/browser/e10s/browser_caching_states.js new file mode 100644 index 0000000000..e6dbc33d65 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_states.js @@ -0,0 +1,154 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * expected {Array} expected states for a given accessible that have the + * following format: + * [ + * expected state, + * expected extra state, + * absent state, + * absent extra state + * ] + * attrs {?Array} an optional list of attributes to update + * } + */ + +// State caching tests for attribute changes +const attributeTests = [ + { + desc: + "Checkbox with @checked attribute set to true should have checked " + + "state", + attrs: [ + { + attr: "checked", + value: "true", + }, + ], + expected: [STATE_CHECKED, 0], + }, + { + desc: "Checkbox with no @checked attribute should not have checked state", + attrs: [ + { + attr: "checked", + }, + ], + expected: [0, 0, STATE_CHECKED], + }, +]; + +// State caching tests for ARIA changes +const ariaTests = [ + { + desc: "File input has busy state when @aria-busy attribute is set to true", + attrs: [ + { + attr: "aria-busy", + value: "true", + }, + ], + expected: [STATE_BUSY, 0, STATE_REQUIRED | STATE_INVALID], + }, + { + desc: + "File input has required state when @aria-required attribute is set " + + "to true", + attrs: [ + { + attr: "aria-required", + value: "true", + }, + ], + expected: [STATE_REQUIRED, 0, STATE_INVALID], + }, + { + desc: + "File input has invalid state when @aria-invalid attribute is set to " + + "true", + attrs: [ + { + attr: "aria-invalid", + value: "true", + }, + ], + expected: [STATE_INVALID, 0], + }, +]; + +// Extra state caching tests +const extraStateTests = [ + { + desc: + "Input has no extra enabled state when aria and native disabled " + + "attributes are set at once", + attrs: [ + { + attr: "aria-disabled", + value: "true", + }, + { + attr: "disabled", + value: "true", + }, + ], + expected: [0, 0, 0, EXT_STATE_ENABLED], + }, + { + desc: + "Input has an extra enabled state when aria and native disabled " + + "attributes are unset at once", + attrs: [ + { + attr: "aria-disabled", + }, + { + attr: "disabled", + }, + ], + expected: [0, EXT_STATE_ENABLED], + }, +]; + +async function runStateTests(browser, accDoc, id, tests) { + let acc = findAccessibleChildByID(accDoc, id); + for (let { desc, attrs, expected } of tests) { + info(desc); + let onUpdate = waitForEvent(EVENT_STATE_CHANGE, id); + for (let { attr, value } of attrs) { + await invokeSetAttribute(browser, id, attr, value); + } + await onUpdate; + testStates(acc, ...expected); + } +} + +/** + * Test caching of accessible object states + */ +addAccessibleTask( + ` + <input id="checkbox" type="checkbox"> + <input id="file" type="file"> + <input id="text">`, + async function(browser, accDoc) { + await runStateTests(browser, accDoc, "checkbox", attributeTests); + await runStateTests(browser, accDoc, "file", ariaTests); + await runStateTests(browser, accDoc, "text", extraStateTests); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_uniqueid.js b/accessible/tests/browser/e10s/browser_caching_uniqueid.js new file mode 100644 index 0000000000..287f896c36 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_uniqueid.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test UniqueID property. + */ +addAccessibleTask( + '<div id="div"></div>', + async function(browser, accDoc) { + const div = findAccessibleChildByID(accDoc, "div"); + const accUniqueID = await invokeContentTask(browser, [], () => { + const accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + + return accService.getAccessibleFor(content.document.getElementById("div")) + .uniqueID; + }); + + is( + accUniqueID, + div.uniqueID, + "Both proxy and the accessible return correct unique ID." + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_caching_value.js b/accessible/tests/browser/e10s/browser_caching_value.js new file mode 100644 index 0000000000..c727fc5252 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_caching_value.js @@ -0,0 +1,195 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/value.js */ +loadScripts({ name: "value.js", dir: MOCHITESTS_DIR }); + +/** + * Test data has the format of: + * { + * desc {String} description for better logging + * id {String} given accessible DOMNode ID + * expected {String} expected value for a given accessible + * action {?AsyncFunction} an optional action that awaits a value change + * attrs {?Array} an optional list of attributes to update + * waitFor {?Number} an optional value change event to wait for + * } + */ +const valueTests = [ + { + desc: "Initially value is set to 1st element of select", + id: "select", + expected: "1st", + }, + { + desc: "Value should update to 3rd when 3 is pressed", + id: "select", + async action(browser) { + await invokeFocus(browser, "select"); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeKey("3", {}, content); + }); + }, + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: "3rd", + }, + { + desc: "Initially value is set to @aria-valuenow for slider", + id: "slider", + expected: ["5", 5, 0, 7, 0], + }, + { + desc: "Value should change when @aria-valuenow is updated", + id: "slider", + attrs: [ + { + attr: "aria-valuenow", + value: "6", + }, + ], + waitFor: EVENT_VALUE_CHANGE, + expected: ["6", 6, 0, 7, 0], + }, + { + desc: "Value should change when @aria-valuetext is set", + id: "slider", + attrs: [ + { + attr: "aria-valuetext", + value: "plain", + }, + ], + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: ["plain", 6, 0, 7, 0], + }, + { + desc: "Value should change when @aria-valuetext is updated", + id: "slider", + attrs: [ + { + attr: "aria-valuetext", + value: "hey!", + }, + ], + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: ["hey!", 6, 0, 7, 0], + }, + { + desc: + "Value should change to @aria-valuetext when @aria-valuenow is removed", + id: "slider", + attrs: [ + { + attr: "aria-valuenow", + }, + ], + expected: ["hey!", 3.5, 0, 7, 0], + }, + { + desc: "Initially value is not set for combobox", + id: "combobox", + expected: "", + }, + { + desc: "Value should change when @value attribute is updated", + id: "combobox", + attrs: [ + { + attr: "value", + value: "hello", + }, + ], + waitFor: EVENT_TEXT_VALUE_CHANGE, + expected: "hello", + }, + { + desc: "Initially value corresponds to @value attribute for progress", + id: "progress", + expected: "22%", + }, + { + desc: "Value should change when @value attribute is updated", + id: "progress", + attrs: [ + { + attr: "value", + value: "50", + }, + ], + waitFor: EVENT_VALUE_CHANGE, + expected: "50%", + }, + { + desc: "Initially value corresponds to @value attribute for range", + id: "range", + expected: "6", + }, + { + desc: "Value should change when slider is moved", + id: "range", + async action(browser) { + await invokeFocus(browser, "range"); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeKey("VK_LEFT", {}, content); + }); + }, + waitFor: EVENT_VALUE_CHANGE, + expected: "5", + }, +]; + +/** + * Test caching of accessible object values + */ +addAccessibleTask( + ` + <div id="slider" role="slider" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="7">slider</div> + <select id="select"> + <option>1st</option> + <option>2nd</option> + <option>3rd</option> + </select> + <input id="combobox" role="combobox" aria-autocomplete="inline"> + <progress id="progress" value="22" max="100"></progress> + <input type="range" id="range" min="0" max="10" value="6">`, + async function(browser, accDoc) { + for (let { desc, id, action, attrs, expected, waitFor } of valueTests) { + info(desc); + let acc = findAccessibleChildByID(accDoc, id); + let onUpdate; + + if (waitFor) { + onUpdate = waitForEvent(waitFor, id); + } + + if (action) { + await action(browser); + } else if (attrs) { + for (let { attr, value } of attrs) { + await invokeSetAttribute(browser, id, attr, value); + } + } + + await onUpdate; + if (Array.isArray(expected)) { + acc.QueryInterface(nsIAccessibleValue); + testValue(acc, ...expected); + } else { + is(acc.value, expected, `Correct value for ${prettyName(acc)}`); + } + } + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_announcement.js b/accessible/tests/browser/e10s/browser_events_announcement.js new file mode 100644 index 0000000000..2de6d4b005 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_announcement.js @@ -0,0 +1,30 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +addAccessibleTask( + `<p id="p">abc</p>`, + async function(browser, accDoc) { + let acc = findAccessibleChildByID(accDoc, "p"); + let onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc); + acc.announce("please", nsIAccessibleAnnouncementEvent.POLITE); + let evt = await onAnnounce; + evt.QueryInterface(nsIAccessibleAnnouncementEvent); + is(evt.announcement, "please", "announcement matches."); + is(evt.priority, nsIAccessibleAnnouncementEvent.POLITE, "priority matches"); + + onAnnounce = waitForEvent(EVENT_ANNOUNCEMENT, acc); + acc.announce("do it", nsIAccessibleAnnouncementEvent.ASSERTIVE); + evt = await onAnnounce; + evt.QueryInterface(nsIAccessibleAnnouncementEvent); + is(evt.announcement, "do it", "announcement matches."); + is( + evt.priority, + nsIAccessibleAnnouncementEvent.ASSERTIVE, + "priority matches" + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_caretmove.js b/accessible/tests/browser/e10s/browser_events_caretmove.js new file mode 100644 index 0000000000..a39d16e710 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_caretmove.js @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test caret move event and its interface: + * - caretOffset + */ +addAccessibleTask( + '<input id="textbox" value="hello"/>', + async function(browser) { + let onCaretMoved = waitForEvent(EVENT_TEXT_CARET_MOVED, "textbox"); + await invokeFocus(browser, "textbox"); + let event = await onCaretMoved; + + let caretMovedEvent = event.QueryInterface(nsIAccessibleCaretMoveEvent); + is(caretMovedEvent.caretOffset, 5, "Correct caret offset."); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_hide.js b/accessible/tests/browser/e10s/browser_events_hide.js new file mode 100644 index 0000000000..d46921d051 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_hide.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test hide event and its interface: + * - targetParent + * - targetNextSibling + * - targetPrevSibling + */ +addAccessibleTask( + ` + <div id="parent"> + <div id="previous"></div> + <div id="to-hide"></div> + <div id="next"></div> + </div>`, + async function(browser, accDoc) { + let acc = findAccessibleChildByID(accDoc, "to-hide"); + let onHide = waitForEvent(EVENT_HIDE, acc); + await invokeSetStyle(browser, "to-hide", "visibility", "hidden"); + let event = await onHide; + let hideEvent = event.QueryInterface(Ci.nsIAccessibleHideEvent); + + is( + getAccessibleDOMNodeID(hideEvent.targetParent), + "parent", + "Correct target parent." + ); + is( + getAccessibleDOMNodeID(hideEvent.targetNextSibling), + "next", + "Correct target next sibling." + ); + is( + getAccessibleDOMNodeID(hideEvent.targetPrevSibling), + "previous", + "Correct target previous sibling." + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_show.js b/accessible/tests/browser/e10s/browser_events_show.js new file mode 100644 index 0000000000..d464d8fb9d --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_show.js @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test show event + */ +addAccessibleTask( + '<div id="div" style="visibility: hidden;"></div>', + async function(browser) { + let onShow = waitForEvent(EVENT_SHOW, "div"); + await invokeSetStyle(browser, "div", "visibility", "visible"); + let showEvent = await onShow; + ok( + showEvent.accessibleDocument instanceof nsIAccessibleDocument, + "Accessible document not present." + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_statechange.js b/accessible/tests/browser/e10s/browser_events_statechange.js new file mode 100644 index 0000000000..a027a974e4 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_statechange.js @@ -0,0 +1,71 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function checkStateChangeEvent(event, state, isExtraState, isEnabled) { + let scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + is(scEvent.state, state, "Correct state of the statechange event."); + is( + scEvent.isExtraState, + isExtraState, + "Correct extra state bit of the statechange event." + ); + is(scEvent.isEnabled, isEnabled, "Correct state of statechange event state"); +} + +// Insert mock source into the iframe to be able to verify the right document +// body id. +let iframeSrc = `data:text/html, + <html> + <head> + <meta charset='utf-8'/> + <title>Inner Iframe</title> + </head> + <body id='iframe'></body> + </html>`; + +/** + * Test state change event and its interface: + * - state + * - isExtraState + * - isEnabled + */ +addAccessibleTask( + ` + <iframe id="iframe" src="${iframeSrc}"></iframe> + <input id="checkbox" type="checkbox" />`, + async function(browser) { + // Test state change + let onStateChange = waitForEvent(EVENT_STATE_CHANGE, "checkbox"); + // Set checked for a checkbox. + await invokeContentTask(browser, [], () => { + content.document.getElementById("checkbox").checked = true; + }); + let event = await onStateChange; + + checkStateChangeEvent(event, STATE_CHECKED, false, true); + testStates(event.accessible, STATE_CHECKED, 0); + + // Test extra state + onStateChange = waitForEvent(EVENT_STATE_CHANGE, "iframe"); + // Set design mode on. + await invokeContentTask(browser, [], () => { + content.document.getElementById("iframe").contentDocument.designMode = + "on"; + }); + event = await onStateChange; + + checkStateChangeEvent(event, EXT_STATE_EDITABLE, true, true); + testStates(event.accessible, 0, EXT_STATE_EDITABLE); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_textchange.js b/accessible/tests/browser/e10s/browser_events_textchange.js new file mode 100644 index 0000000000..1e822dc65a --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_textchange.js @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function checkTextChangeEvent( + event, + id, + text, + start, + end, + isInserted, + isFromUserInput +) { + let tcEvent = event.QueryInterface(nsIAccessibleTextChangeEvent); + is(tcEvent.start, start, `Correct start offset for ${prettyName(id)}`); + is(tcEvent.length, end - start, `Correct length for ${prettyName(id)}`); + is( + tcEvent.isInserted, + isInserted, + `Correct isInserted flag for ${prettyName(id)}` + ); + is(tcEvent.modifiedText, text, `Correct text for ${prettyName(id)}`); + is( + tcEvent.isFromUserInput, + isFromUserInput, + `Correct value of isFromUserInput for ${prettyName(id)}` + ); + ok( + tcEvent.accessibleDocument instanceof nsIAccessibleDocument, + "Accessible document not present." + ); +} + +async function changeText(browser, id, value, events) { + let onEvents = waitForOrderedEvents( + events.map(({ isInserted }) => { + let eventType = isInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED; + return [eventType, id]; + }) + ); + // Change text in the subtree. + await invokeContentTask(browser, [id, value], (contentId, contentValue) => { + content.document.getElementById( + contentId + ).firstChild.textContent = contentValue; + }); + let resolvedEvents = await onEvents; + + events.forEach(({ isInserted, str, offset }, idx) => + checkTextChangeEvent( + resolvedEvents[idx], + id, + str, + offset, + offset + str.length, + isInserted, + false + ) + ); +} + +async function removeTextFromInput(browser, id, value, start, end) { + let onTextRemoved = waitForEvent(EVENT_TEXT_REMOVED, id); + // Select text and delete it. + await invokeContentTask( + browser, + [id, start, end], + (contentId, contentStart, contentEnd) => { + let el = content.document.getElementById(contentId); + el.focus(); + el.setSelectionRange(contentStart, contentEnd); + } + ); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.sendChar("VK_DELETE", content); + }); + + let event = await onTextRemoved; + checkTextChangeEvent(event, id, value, start, end, false, true); +} + +/** + * Test text change event and its interface: + * - start + * - length + * - isInserted + * - modifiedText + * - isFromUserInput + */ +addAccessibleTask( + ` + <p id="p">abc</p> + <input id="input" value="input" />`, + async function(browser) { + let events = [ + { isInserted: false, str: "abc", offset: 0 }, + { isInserted: true, str: "def", offset: 0 }, + ]; + await changeText(browser, "p", "def", events); + + events = [{ isInserted: true, str: "DEF", offset: 2 }]; + await changeText(browser, "p", "deDEFf", events); + + // Test isFromUserInput property. + await removeTextFromInput(browser, "input", "n", 1, 2); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_events_vcchange.js b/accessible/tests/browser/e10s/browser_events_vcchange.js new file mode 100644 index 0000000000..bf649fe045 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_events_vcchange.js @@ -0,0 +1,87 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +addAccessibleTask( + ` + <p id="p1">abc</p> + <input id="input1" value="input" />`, + async function(browser) { + let onVCChanged = waitForEvent( + EVENT_VIRTUALCURSOR_CHANGED, + matchContentDoc + ); + await invokeContentTask(browser, [], () => { + const { CommonUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm" + ); + let vc = CommonUtils.getAccessible( + content.document, + Ci.nsIAccessibleDocument + ).virtualCursor; + vc.position = CommonUtils.getAccessible( + "p1", + null, + null, + null, + content.document + ); + }); + let vccEvent = (await onVCChanged).QueryInterface( + nsIAccessibleVirtualCursorChangeEvent + ); + is(vccEvent.newAccessible.id, "p1", "New position is correct"); + is(vccEvent.newStartOffset, -1, "New start offset is correct"); + is(vccEvent.newEndOffset, -1, "New end offset is correct"); + ok(!vccEvent.isFromUserInput, "not user initiated"); + + onVCChanged = waitForEvent(EVENT_VIRTUALCURSOR_CHANGED, matchContentDoc); + await invokeContentTask(browser, [], () => { + const { CommonUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm" + ); + let vc = CommonUtils.getAccessible( + content.document, + Ci.nsIAccessibleDocument + ).virtualCursor; + vc.moveNextByText(Ci.nsIAccessiblePivot.CHAR_BOUNDARY); + }); + vccEvent = (await onVCChanged).QueryInterface( + nsIAccessibleVirtualCursorChangeEvent + ); + is(vccEvent.newAccessible.id, vccEvent.oldAccessible.id, "Same position"); + is(vccEvent.newStartOffset, 0, "New start offset is correct"); + is(vccEvent.newEndOffset, 1, "New end offset is correct"); + ok(vccEvent.isFromUserInput, "user initiated"); + + onVCChanged = waitForEvent(EVENT_VIRTUALCURSOR_CHANGED, matchContentDoc); + await invokeContentTask(browser, [], () => { + const { CommonUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm" + ); + let vc = CommonUtils.getAccessible( + content.document, + Ci.nsIAccessibleDocument + ).virtualCursor; + vc.position = CommonUtils.getAccessible( + "input1", + null, + null, + null, + content.document + ); + }); + vccEvent = (await onVCChanged).QueryInterface( + nsIAccessibleVirtualCursorChangeEvent + ); + isnot(vccEvent.oldAccessible, vccEvent.newAccessible, "positions differ"); + is(vccEvent.oldAccessible.id, "p1", "Old position is correct"); + is(vccEvent.newAccessible.id, "input1", "New position is correct"); + is(vccEvent.newStartOffset, -1, "New start offset is correct"); + is(vccEvent.newEndOffset, -1, "New end offset is correct"); + ok(!vccEvent.isFromUserInput, "not user initiated"); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_text_paragraph_boundary.js b/accessible/tests/browser/e10s/browser_text_paragraph_boundary.js new file mode 100644 index 0000000000..04e64520e8 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_text_paragraph_boundary.js @@ -0,0 +1,22 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Test that we don't crash the parent process when querying the paragraph +// boundary on an Accessible which has remote ProxyAccessible descendants. +addAccessibleTask( + `test`, + async function testParagraphBoundaryWithRemoteDescendants(browser, accDoc) { + const root = getRootAccessible(document).QueryInterface( + Ci.nsIAccessibleText + ); + let start = {}; + let end = {}; + // The offsets will change as the Firefox UI changes. We don't really care + // what they are, just that we don't crash. + root.getTextAtOffset(0, nsIAccessibleText.BOUNDARY_PARAGRAPH, start, end); + ok(true, "Getting paragraph boundary succeeded"); + } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js b/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js new file mode 100644 index 0000000000..8b4a575d75 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_ariadialog.js @@ -0,0 +1,45 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +// Test ARIA Dialog +addAccessibleTask( + "e10s/doc_treeupdate_ariadialog.html", + async function(browser, accDoc) { + testAccessibleTree(accDoc, { + role: ROLE_DOCUMENT, + children: [], + }); + + // Make dialog visible and update its inner content. + let onShow = waitForEvent(EVENT_SHOW, "dialog"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("dialog").style.display = "block"; + }); + await onShow; + + testAccessibleTree(accDoc, { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_DIALOG, + children: [ + { + role: ROLE_PUSHBUTTON, + children: [{ role: ROLE_TEXT_LEAF }], + }, + { + role: ROLE_ENTRY, + }, + ], + }, + ], + }); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js new file mode 100644 index 0000000000..dfd6401c48 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_ariaowns.js @@ -0,0 +1,294 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function testContainer1(browser, accDoc) { + const id = "t1_container"; + const docID = getAccessibleDOMNodeID(accDoc); + const acc = findAccessibleChildByID(accDoc, id); + + /* ================= Initial tree test ==================================== */ + // children are swapped by ARIA owns + let tree = { + SECTION: [{ CHECKBUTTON: [{ SECTION: [] }] }, { PUSHBUTTON: [] }], + }; + testAccessibleTree(acc, tree); + + /* ================ Change ARIA owns ====================================== */ + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv"); + await onReorder; + + // children are swapped again, button and subdiv are appended to + // the children. + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // checkbox, native order + { PUSHBUTTON: [] }, // button, rearranged by ARIA own + { SECTION: [] }, // subdiv from the subtree, ARIA owned + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Remove ARIA owns ====================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, id, "aria-owns"); + await onReorder; + + // children follow the DOM order + tree = { + SECTION: [{ PUSHBUTTON: [] }, { CHECKBUTTON: [{ SECTION: [] }] }], + }; + testAccessibleTree(acc, tree); + + /* ================ Set ARIA owns ========================================= */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, id, "aria-owns", "t1_button t1_subdiv"); + await onReorder; + + // children are swapped again, button and subdiv are appended to + // the children. + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // checkbox + { PUSHBUTTON: [] }, // button, rearranged by ARIA own + { SECTION: [] }, // subdiv from the subtree, ARIA owned + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Add ID to ARIA owns =================================== */ + onReorder = waitForEvent(EVENT_REORDER, docID); + await invokeSetAttribute( + browser, + id, + "aria-owns", + "t1_button t1_subdiv t1_group" + ); + await onReorder; + + // children are swapped again, button and subdiv are appended to + // the children. + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // t1_checkbox + { PUSHBUTTON: [] }, // button, t1_button + { SECTION: [] }, // subdiv from the subtree, t1_subdiv + { GROUPING: [] }, // group from outside, t1_group + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Append element ======================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let div = content.document.createElement("div"); + div.setAttribute("id", "t1_child3"); + div.setAttribute("role", "radio"); + content.document.getElementById(contentId).appendChild(div); + }); + await onReorder; + + // children are invalidated, they includes aria-owns swapped kids and + // newly inserted child. + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // existing explicit, t1_checkbox + { RADIOBUTTON: [] }, // new explicit, t1_child3 + { PUSHBUTTON: [] }, // ARIA owned, t1_button + { SECTION: [] }, // ARIA owned, t1_subdiv + { GROUPING: [] }, // ARIA owned, t1_group + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Remove element ======================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + content.document.getElementById("t1_span").remove(); + }); + await onReorder; + + // subdiv should go away + tree = { + SECTION: [ + { CHECKBUTTON: [] }, // explicit, t1_checkbox + { RADIOBUTTON: [] }, // explicit, t1_child3 + { PUSHBUTTON: [] }, // ARIA owned, t1_button + { GROUPING: [] }, // ARIA owned, t1_group + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Remove ID ============================================= */ + onReorder = waitForEvent(EVENT_REORDER, docID); + await invokeSetAttribute(browser, "t1_group", "id"); + await onReorder; + + tree = { + SECTION: [ + { CHECKBUTTON: [] }, + { RADIOBUTTON: [] }, + { PUSHBUTTON: [] }, // ARIA owned, t1_button + ], + }; + testAccessibleTree(acc, tree); + + /* ================ Set ID ================================================ */ + onReorder = waitForEvent(EVENT_REORDER, docID); + await invokeSetAttribute(browser, "t1_grouptmp", "id", "t1_group"); + await onReorder; + + tree = { + SECTION: [ + { CHECKBUTTON: [] }, + { RADIOBUTTON: [] }, + { PUSHBUTTON: [] }, // ARIA owned, t1_button + { GROUPING: [] }, // ARIA owned, t1_group, previously t1_grouptmp + ], + }; + testAccessibleTree(acc, tree); +} + +async function removeContainer(browser, accDoc) { + const id = "t2_container1"; + const acc = findAccessibleChildByID(accDoc, id); + + let tree = { + SECTION: [ + { CHECKBUTTON: [] }, // ARIA owned, 't2_owned' + ], + }; + testAccessibleTree(acc, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + content.document + .getElementById("t2_container2") + .removeChild(content.document.getElementById("t2_container3")); + }); + await onReorder; + + tree = { + SECTION: [], + }; + testAccessibleTree(acc, tree); +} + +async function stealAndRecacheChildren(browser, accDoc) { + const id1 = "t3_container1"; + const id2 = "t3_container2"; + const acc1 = findAccessibleChildByID(accDoc, id1); + const acc2 = findAccessibleChildByID(accDoc, id2); + + /* ================ Attempt to steal from other ARIA owns ================= */ + let onReorder = waitForEvent(EVENT_REORDER, id2); + await invokeSetAttribute(browser, id2, "aria-owns", "t3_child"); + await invokeContentTask(browser, [id2], id => { + let div = content.document.createElement("div"); + div.setAttribute("role", "radio"); + content.document.getElementById(id).appendChild(div); + }); + await onReorder; + + let tree = { + SECTION: [ + { CHECKBUTTON: [] }, // ARIA owned + ], + }; + testAccessibleTree(acc1, tree); + + tree = { + SECTION: [{ RADIOBUTTON: [] }], + }; + testAccessibleTree(acc2, tree); +} + +async function showHiddenElement(browser, accDoc) { + const id = "t4_container1"; + const acc = findAccessibleChildByID(accDoc, id); + + let tree = { + SECTION: [{ RADIOBUTTON: [] }], + }; + testAccessibleTree(acc, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetStyle(browser, "t4_child1", "display", "block"); + await onReorder; + + tree = { + SECTION: [{ CHECKBUTTON: [] }, { RADIOBUTTON: [] }], + }; + testAccessibleTree(acc, tree); +} + +async function rearrangeARIAOwns(browser, accDoc) { + const id = "t5_container"; + const acc = findAccessibleChildByID(accDoc, id); + const tests = [ + { + val: "t5_checkbox t5_radio t5_button", + roleList: ["CHECKBUTTON", "RADIOBUTTON", "PUSHBUTTON"], + }, + { + val: "t5_radio t5_button t5_checkbox", + roleList: ["RADIOBUTTON", "PUSHBUTTON", "CHECKBUTTON"], + }, + ]; + + for (let { val, roleList } of tests) { + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, id, "aria-owns", val); + await onReorder; + + let tree = { SECTION: [] }; + for (let role of roleList) { + let ch = {}; + ch[role] = []; + tree.SECTION.push(ch); + } + testAccessibleTree(acc, tree); + } +} + +async function removeNotARIAOwnedEl(browser, accDoc) { + const id = "t6_container"; + const acc = findAccessibleChildByID(accDoc, id); + + let tree = { + SECTION: [{ TEXT_LEAF: [] }, { GROUPING: [] }], + }; + testAccessibleTree(acc, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + content.document + .getElementById(contentId) + .removeChild(content.document.getElementById("t6_span")); + }); + await onReorder; + + tree = { + SECTION: [{ GROUPING: [] }], + }; + testAccessibleTree(acc, tree); +} + +addAccessibleTask( + "e10s/doc_treeupdate_ariaowns.html", + async function(browser, accDoc) { + await testContainer1(browser, accDoc); + await removeContainer(browser, accDoc); + await stealAndRecacheChildren(browser, accDoc); + await showHiddenElement(browser, accDoc); + await rearrangeARIAOwns(browser, accDoc); + await removeNotARIAOwnedEl(browser, accDoc); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_canvas.js b/accessible/tests/browser/e10s/browser_treeupdate_canvas.js new file mode 100644 index 0000000000..5fcd1eb773 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_canvas.js @@ -0,0 +1,28 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` + <canvas id="canvas"> + <div id="dialog" role="dialog" style="display: none;"></div> + </canvas>`, + async function(browser, accDoc) { + let canvas = findAccessibleChildByID(accDoc, "canvas"); + let dialog = findAccessibleChildByID(accDoc, "dialog"); + + testAccessibleTree(canvas, { CANVAS: [] }); + + let onShow = waitForEvent(EVENT_SHOW, "dialog"); + await invokeSetStyle(browser, "dialog", "display", "block"); + await onShow; + + testAccessibleTree(dialog, { DIALOG: [] }); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js b/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js new file mode 100644 index 0000000000..629f9fb89f --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_cssoverflow.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` + <div id="container"><div id="scrollarea" style="overflow:auto;"><input>`, + async function(browser, accDoc) { + const id1 = "container"; + const container = findAccessibleChildByID(accDoc, id1); + + /* ================= Change scroll range ================================== */ + let tree = { + SECTION: [ + { + // container + SECTION: [ + { + // scroll area + ENTRY: [], // child content + }, + ], + }, + ], + }; + testAccessibleTree(container, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id1); + await invokeContentTask(browser, [id1], id => { + let doc = content.document; + doc.getElementById("scrollarea").style.width = "20px"; + doc.getElementById(id).appendChild(doc.createElement("input")); + }); + await onReorder; + + tree = { + SECTION: [ + { + // container + SECTION: [ + { + // scroll area + ENTRY: [], // child content + }, + ], + }, + { + ENTRY: [], // inserted input + }, + ], + }; + testAccessibleTree(container, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_doc.js b/accessible/tests/browser/e10s/browser_treeupdate_doc.js new file mode 100644 index 0000000000..98f399695c --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_doc.js @@ -0,0 +1,320 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +const iframeSrc = `data:text/html, + <html> + <head> + <meta charset='utf-8'/> + <title>Inner Iframe</title> + </head> + <body id='inner-iframe'></body> + </html>`; + +addAccessibleTask( + ` + <iframe id="iframe" src="${iframeSrc}"></iframe>`, + async function(browser, accDoc) { + // ID of the iframe that is being tested + const id = "inner-iframe"; + + let iframe = findAccessibleChildByID(accDoc, id); + + /* ================= Initial tree check =================================== */ + let tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================= Write iframe document ================================ */ + let reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let docNode = content.document.getElementById("iframe").contentDocument; + let newHTMLNode = docNode.createElement("html"); + let newBodyNode = docNode.createElement("body"); + let newTextNode = docNode.createTextNode("New Wave"); + newBodyNode.id = contentId; + newBodyNode.appendChild(newTextNode); + newHTMLNode.appendChild(newBodyNode); + docNode.replaceChild(newHTMLNode, docNode.documentElement); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "New Wave", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Replace iframe HTML element ========================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let docNode = content.document.getElementById("iframe").contentDocument; + // We can't use open/write/close outside of iframe document because of + // security error. + let script = docNode.createElement("script"); + script.textContent = ` + document.open(); + document.write('<body id="${contentId}">hello</body>'); + document.close();`; + docNode.body.appendChild(script); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "hello", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Replace iframe body ================================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let docNode = content.document.getElementById("iframe").contentDocument; + let newBodyNode = docNode.createElement("body"); + let newTextNode = docNode.createTextNode("New Hello"); + newBodyNode.id = contentId; + newBodyNode.appendChild(newTextNode); + newBodyNode.setAttribute("role", "application"); + docNode.documentElement.replaceChild(newBodyNode, docNode.body); + }); + await reorderEventPromise; + + tree = { + role: ROLE_APPLICATION, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "New Hello", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Open iframe document ================================= */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + // Open document. + let docNode = content.document.getElementById("iframe").contentDocument; + let script = docNode.createElement("script"); + script.textContent = ` + function closeMe() { + document.write('Works?'); + document.close(); + } + window.closeMe = closeMe; + document.open(); + document.write('<body id="${contentId}"></body>');`; + docNode.body.appendChild(script); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================= Close iframe document ================================ */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + // Write and close document. + let docNode = content.document.getElementById("iframe").contentDocument; + docNode.write("Works?"); + docNode.close(); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Works?", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Remove HTML from iframe document ===================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + // Remove HTML element. + let docNode = content.document.getElementById("iframe").contentDocument; + docNode.firstChild.remove(); + }); + let event = await reorderEventPromise; + + ok( + event.accessible instanceof nsIAccessibleDocument, + "Reorder should happen on the document" + ); + tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================= Insert HTML to iframe document ======================= */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + // Insert HTML element. + let docNode = content.document.getElementById("iframe").contentDocument; + let html = docNode.createElement("html"); + let body = docNode.createElement("body"); + let text = docNode.createTextNode("Haha"); + body.appendChild(text); + body.id = contentId; + html.appendChild(body); + docNode.appendChild(html); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Haha", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Remove body from iframe document ===================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + // Remove body element. + let docNode = content.document.getElementById("iframe").contentDocument; + docNode.documentElement.removeChild(docNode.body); + }); + event = await reorderEventPromise; + + ok( + event.accessible instanceof nsIAccessibleDocument, + "Reorder should happen on the document" + ); + tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================ Insert element under document element while body missed */ + reorderEventPromise = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + let docNode = content.document.getElementById("iframe").contentDocument; + let inputNode = (content.window.inputNode = docNode.createElement( + "input" + )); + docNode.documentElement.appendChild(inputNode); + }); + event = await reorderEventPromise; + + ok( + event.accessible instanceof nsIAccessibleDocument, + "Reorder should happen on the document" + ); + tree = { + DOCUMENT: [{ ENTRY: [] }], + }; + testAccessibleTree(iframe, tree); + + reorderEventPromise = waitForEvent(EVENT_REORDER, iframe); + await invokeContentTask(browser, [], () => { + let docEl = content.document.getElementById("iframe").contentDocument + .documentElement; + // Remove aftermath of this test before next test starts. + docEl.firstChild.remove(); + }); + // Make sure reorder event was fired and that the input was removed. + await reorderEventPromise; + tree = { + role: ROLE_DOCUMENT, + children: [], + }; + testAccessibleTree(iframe, tree); + + /* ================= Insert body to iframe document ======================= */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + // Write and close document. + let docNode = content.document.getElementById("iframe").contentDocument; + // Insert body element. + let body = docNode.createElement("body"); + let text = docNode.createTextNode("Yo ho ho i butylka roma!"); + body.appendChild(text); + body.id = contentId; + docNode.documentElement.appendChild(body); + }); + await reorderEventPromise; + + tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Yo ho ho i butylka roma!", + }, + ], + }; + testAccessibleTree(iframe, tree); + + /* ================= Change source ======================================== */ + reorderEventPromise = waitForEvent(EVENT_REORDER, "iframe"); + await invokeSetAttribute( + browser, + "iframe", + "src", + `data:text/html,<html><body id="${id}"><input></body></html>` + ); + event = await reorderEventPromise; + + tree = { + INTERNAL_FRAME: [{ DOCUMENT: [{ ENTRY: [] }] }], + }; + testAccessibleTree(event.accessible, tree); + iframe = findAccessibleChildByID(event.accessible, id); + + /* ================= Replace iframe body on ARIA role body ================ */ + reorderEventPromise = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let docNode = content.document.getElementById("iframe").contentDocument; + let newBodyNode = docNode.createElement("body"); + let newTextNode = docNode.createTextNode("New Hello"); + newBodyNode.appendChild(newTextNode); + newBodyNode.setAttribute("role", "application"); + newBodyNode.id = contentId; + docNode.documentElement.replaceChild(newBodyNode, docNode.body); + }); + await reorderEventPromise; + + tree = { + role: ROLE_APPLICATION, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "New Hello", + }, + ], + }; + testAccessibleTree(iframe, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js b/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js new file mode 100644 index 0000000000..ca1150f9dd --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_gencontent.js @@ -0,0 +1,94 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` + <style> + .gentext:before { + content: "START" + } + .gentext:after { + content: "END" + } + </style> + <div id="container1"></div> + <div id="container2"><div id="container2_child">text</div></div>`, + async function(browser, accDoc) { + const id1 = "container1"; + const id2 = "container2"; + let container1 = findAccessibleChildByID(accDoc, id1); + let container2 = findAccessibleChildByID(accDoc, id2); + + let tree = { + SECTION: [], // container + }; + testAccessibleTree(container1, tree); + + tree = { + SECTION: [ + { + // container2 + SECTION: [ + { + // container2 child + TEXT_LEAF: [], // primary text + }, + ], + }, + ], + }; + testAccessibleTree(container2, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id1); + // Create and add an element with CSS generated content to container1 + await invokeContentTask(browser, [id1], id => { + let node = content.document.createElement("div"); + node.textContent = "text"; + node.setAttribute("class", "gentext"); + content.document.getElementById(id).appendChild(node); + }); + await onReorder; + + tree = { + SECTION: [ + // container + { + SECTION: [ + // inserted node + { STATICTEXT: [] }, // :before + { TEXT_LEAF: [] }, // primary text + { STATICTEXT: [] }, // :after + ], + }, + ], + }; + testAccessibleTree(container1, tree); + + onReorder = waitForEvent(EVENT_REORDER, "container2_child"); + // Add CSS generated content to an element in container2's subtree + await invokeSetAttribute(browser, "container2_child", "class", "gentext"); + await onReorder; + + tree = { + SECTION: [ + // container2 + { + SECTION: [ + // container2 child + { STATICTEXT: [] }, // :before + { TEXT_LEAF: [] }, // primary text + { STATICTEXT: [] }, // :after + ], + }, + ], + }; + testAccessibleTree(container2, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_hidden.js b/accessible/tests/browser/e10s/browser_treeupdate_hidden.js new file mode 100644 index 0000000000..725999db36 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_hidden.js @@ -0,0 +1,32 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function setHidden(browser, value) { + let onReorder = waitForEvent(EVENT_REORDER, "container"); + await invokeSetAttribute(browser, "child", "hidden", value); + await onReorder; +} + +addAccessibleTask( + '<div id="container"><input id="child"></div>', + async function(browser, accDoc) { + let container = findAccessibleChildByID(accDoc, "container"); + + testAccessibleTree(container, { SECTION: [{ ENTRY: [] }] }); + + // Set @hidden attribute + await setHidden(browser, "true"); + testAccessibleTree(container, { SECTION: [] }); + + // Remove @hidden attribute + await setHidden(browser); + testAccessibleTree(container, { SECTION: [{ ENTRY: [] }] }); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js new file mode 100644 index 0000000000..e206b83276 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_imagemap.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function testImageMap(browser, accDoc) { + const id = "imgmap"; + const acc = findAccessibleChildByID(accDoc, id); + + /* ================= Initial tree test ==================================== */ + let tree = { + IMAGE_MAP: [{ role: ROLE_LINK, name: "b", children: [] }], + }; + testAccessibleTree(acc, tree); + + /* ================= Insert area ========================================== */ + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + let areaElm = content.document.createElement("area"); + let mapNode = content.document.getElementById("map"); + areaElm.setAttribute( + "href", + "http://www.bbc.co.uk/radio4/atoz/index.shtml#a" + ); + areaElm.setAttribute("coords", "0,0,13,14"); + areaElm.setAttribute("alt", "a"); + areaElm.setAttribute("shape", "rect"); + mapNode.insertBefore(areaElm, mapNode.firstChild); + }); + await onReorder; + + tree = { + IMAGE_MAP: [ + { role: ROLE_LINK, name: "a", children: [] }, + { role: ROLE_LINK, name: "b", children: [] }, + ], + }; + testAccessibleTree(acc, tree); + + /* ================= Append area ========================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + let areaElm = content.document.createElement("area"); + let mapNode = content.document.getElementById("map"); + areaElm.setAttribute( + "href", + "http://www.bbc.co.uk/radio4/atoz/index.shtml#c" + ); + areaElm.setAttribute("coords", "34,0,47,14"); + areaElm.setAttribute("alt", "c"); + areaElm.setAttribute("shape", "rect"); + mapNode.appendChild(areaElm); + }); + await onReorder; + + tree = { + IMAGE_MAP: [ + { role: ROLE_LINK, name: "a", children: [] }, + { role: ROLE_LINK, name: "b", children: [] }, + { role: ROLE_LINK, name: "c", children: [] }, + ], + }; + testAccessibleTree(acc, tree); + + /* ================= Remove area ========================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + let mapNode = content.document.getElementById("map"); + mapNode.removeChild(mapNode.firstElementChild); + }); + await onReorder; + + tree = { + IMAGE_MAP: [ + { role: ROLE_LINK, name: "b", children: [] }, + { role: ROLE_LINK, name: "c", children: [] }, + ], + }; + testAccessibleTree(acc, tree); +} + +async function testContainer(browser) { + const id = "container"; + /* ================= Remove name on map =================================== */ + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, "map", "name"); + let event = await onReorder; + const acc = event.accessible; + + let tree = { + SECTION: [{ GRAPHIC: [] }], + }; + testAccessibleTree(acc, tree); + + /* ================= Restore name on map ================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetAttribute(browser, "map", "name", "atoz_map"); + // XXX: force repainting of the image (see bug 745788 for details). + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeMouse( + content.document.getElementById("imgmap"), + 10, + 10, + { type: "mousemove" }, + content + ); + }); + await onReorder; + + tree = { + SECTION: [ + { + IMAGE_MAP: [{ LINK: [] }, { LINK: [] }], + }, + ], + }; + testAccessibleTree(acc, tree); + + /* ================= Remove map =========================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [], () => { + let mapNode = content.document.getElementById("map"); + mapNode.remove(); + }); + await onReorder; + + tree = { + SECTION: [{ GRAPHIC: [] }], + }; + testAccessibleTree(acc, tree); + + /* ================= Insert map =========================================== */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + let map = content.document.createElement("map"); + let area = content.document.createElement("area"); + + map.setAttribute("name", "atoz_map"); + map.setAttribute("id", "map"); + + area.setAttribute("href", "http://www.bbc.co.uk/radio4/atoz/index.shtml#b"); + area.setAttribute("coords", "17,0,30,14"); + area.setAttribute("alt", "b"); + area.setAttribute("shape", "rect"); + + map.appendChild(area); + content.document.getElementById(contentId).appendChild(map); + }); + await onReorder; + + tree = { + SECTION: [ + { + IMAGE_MAP: [{ LINK: [] }], + }, + ], + }; + testAccessibleTree(acc, tree); + + /* ================= Hide image map ======================================= */ + onReorder = waitForEvent(EVENT_REORDER, id); + await invokeSetStyle(browser, "imgmap", "display", "none"); + await onReorder; + + tree = { + SECTION: [], + }; + testAccessibleTree(acc, tree); +} + +addAccessibleTask( + "e10s/doc_treeupdate_imagemap.html", + async function(browser, accDoc) { + await waitForImageMap(browser, accDoc); + await testImageMap(browser, accDoc); + await testContainer(browser); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_list.js b/accessible/tests/browser/e10s/browser_treeupdate_list.js new file mode 100644 index 0000000000..d14b983c10 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_list.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function setDisplayAndWaitForReorder(browser, value) { + let onReorder = waitForEvent(EVENT_REORDER, "ul"); + await invokeSetStyle(browser, "li", "display", value); + return onReorder; +} + +addAccessibleTask( + ` + <ul id="ul"> + <li id="li">item1</li> + </ul>`, + async function(browser, accDoc) { + let li = findAccessibleChildByID(accDoc, "li"); + let bullet = li.firstChild; + let accTree = { + role: ROLE_LISTITEM, + children: [ + { + role: ROLE_LISTITEM_MARKER, + children: [], + }, + { + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + testAccessibleTree(li, accTree); + + await setDisplayAndWaitForReorder(browser, "none"); + + ok(isDefunct(li), "Check that li is defunct."); + ok(isDefunct(bullet), "Check that bullet is defunct."); + + let event = await setDisplayAndWaitForReorder(browser, "list-item"); + + testAccessibleTree( + findAccessibleChildByID(event.accessible, "li"), + accTree + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js b/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js new file mode 100644 index 0000000000..9c672f3c7c --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_list_editabledoc.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + '<ol id="list"></ol>', + async function(browser, accDoc) { + let list = findAccessibleChildByID(accDoc, "list"); + + testAccessibleTree(list, { + role: ROLE_LIST, + children: [], + }); + + await invokeSetAttribute( + browser, + currentContentDoc(), + "contentEditable", + "true" + ); + let onReorder = waitForEvent(EVENT_REORDER, "list"); + await invokeContentTask(browser, [], () => { + let li = content.document.createElement("li"); + li.textContent = "item"; + content.document.getElementById("list").appendChild(li); + }); + await onReorder; + + testAccessibleTree(list, { + role: ROLE_LIST, + children: [ + { + role: ROLE_LISTITEM, + children: [ + { role: ROLE_LISTITEM_MARKER, name: "1. ", children: [] }, + { role: ROLE_TEXT_LEAF, children: [] }, + ], + }, + ], + }); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_listener.js b/accessible/tests/browser/e10s/browser_treeupdate_listener.js new file mode 100644 index 0000000000..35baf28667 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_listener.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + '<span id="parent"><span id="child"></span></span>', + async function(browser, accDoc) { + is( + findAccessibleChildByID(accDoc, "parent"), + null, + "Check that parent is not accessible." + ); + is( + findAccessibleChildByID(accDoc, "child"), + null, + "Check that child is not accessible." + ); + + let onReorder = waitForEvent(EVENT_REORDER, matchContentDoc); + // Add an event listener to parent. + await invokeContentTask(browser, [], () => { + content.window.dummyListener = () => {}; + content.document + .getElementById("parent") + .addEventListener("click", content.window.dummyListener); + }); + await onReorder; + + let tree = { TEXT: [] }; + testAccessibleTree(findAccessibleChildByID(accDoc, "parent"), tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js new file mode 100644 index 0000000000..15fed8112d --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_optgroup.js @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + '<select id="select"></select>', + async function(browser, accDoc) { + let select = findAccessibleChildByID(accDoc, "select"); + + let onEvent = waitForEvent(EVENT_REORDER, "select"); + // Create a combobox with grouping and 2 standalone options + await invokeContentTask(browser, [], () => { + let doc = content.document; + let contentSelect = doc.getElementById("select"); + let optGroup = doc.createElement("optgroup"); + + for (let i = 0; i < 2; i++) { + let opt = doc.createElement("option"); + opt.value = i; + opt.text = "Option: Value " + i; + optGroup.appendChild(opt); + } + contentSelect.add(optGroup, null); + + for (let i = 0; i < 2; i++) { + let opt = doc.createElement("option"); + contentSelect.add(opt, null); + } + contentSelect.firstChild.firstChild.id = "option1Node"; + }); + let event = await onEvent; + let option1Node = findAccessibleChildByID(event.accessible, "option1Node"); + + let tree = { + COMBOBOX: [ + { + COMBOBOX_LIST: [ + { + GROUPING: [ + { COMBOBOX_OPTION: [{ TEXT_LEAF: [] }] }, + { COMBOBOX_OPTION: [{ TEXT_LEAF: [] }] }, + ], + }, + { + COMBOBOX_OPTION: [], + }, + { + COMBOBOX_OPTION: [], + }, + ], + }, + ], + }; + testAccessibleTree(select, tree); + ok(!isDefunct(option1Node), "option shouldn't be defunct"); + + onEvent = waitForEvent(EVENT_REORDER, "select"); + // Remove grouping from combobox + await invokeContentTask(browser, [], () => { + let contentSelect = content.document.getElementById("select"); + contentSelect.firstChild.remove(); + }); + await onEvent; + + tree = { + COMBOBOX: [ + { + COMBOBOX_LIST: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }], + }, + ], + }; + testAccessibleTree(select, tree); + ok( + isDefunct(option1Node), + "removed option shouldn't be accessible anymore!" + ); + + onEvent = waitForEvent(EVENT_REORDER, "select"); + // Remove all options from combobox + await invokeContentTask(browser, [], () => { + let contentSelect = content.document.getElementById("select"); + while (contentSelect.length) { + contentSelect.remove(0); + } + }); + await onEvent; + + tree = { + COMBOBOX: [ + { + COMBOBOX_LIST: [], + }, + ], + }; + testAccessibleTree(select, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_removal.js b/accessible/tests/browser/e10s/browser_treeupdate_removal.js new file mode 100644 index 0000000000..eb791525b3 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_removal.js @@ -0,0 +1,58 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + "e10s/doc_treeupdate_removal.xhtml", + async function(browser, accDoc) { + ok( + isAccessible(findAccessibleChildByID(accDoc, "the_table")), + "table should be accessible" + ); + + // Move the_table element into hidden subtree. + let onReorder = waitForEvent(EVENT_REORDER, matchContentDoc); + await invokeContentTask(browser, [], () => { + content.document + .getElementById("the_displaynone") + .appendChild(content.document.getElementById("the_table")); + }); + await onReorder; + + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_table")), + "table in display none tree shouldn't be accessible" + ); + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_row")), + "row shouldn't be accessible" + ); + + // Remove the_row element (since it did not have accessible, no event needed). + await invokeContentTask(browser, [], () => { + content.document.body.removeChild( + content.document.getElementById("the_row") + ); + }); + + // make sure no accessibles have stuck around. + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_row")), + "row shouldn't be accessible" + ); + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_table")), + "table shouldn't be accessible" + ); + ok( + !isAccessible(findAccessibleChildByID(accDoc, "the_displayNone")), + "display none things shouldn't be accessible" + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js new file mode 100644 index 0000000000..add454ceac --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_select_dropdown.js @@ -0,0 +1,77 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +const snippet = ` +<select id="select"> + <option>o1</option> + <optgroup label="g1"> + <option>g1o1</option> + <option>g1o2</option> + </optgroup> + <optgroup label="g2"> + <option>g2o1</option> + <option>g2o2</option> + </optgroup> + <option>o2</option> +</select> +`; + +addAccessibleTask( + snippet, + async function(browser, accDoc) { + await invokeFocus(browser, "select"); + // Expand the select. A dropdown item should get focus. + // Note that the dropdown is rendered in the parent process. + let focused = waitForEvent( + EVENT_FOCUS, + event => event.accessible.role == ROLE_COMBOBOX_OPTION, + "Dropdown item focused after select expanded" + ); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeKey("VK_DOWN", { altKey: true }, content); + }); + let event = await focused; + let dropdown = event.accessible.parent; + + let selectedOptionChildren = []; + if (MAC) { + // Checkmark is part of the Mac menu styling. + selectedOptionChildren = [{ STATICTEXT: [] }]; + } + let tree = { + COMBOBOX_LIST: [ + { COMBOBOX_OPTION: selectedOptionChildren }, + { GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] }, + { GROUPING: [{ COMBOBOX_OPTION: [] }, { COMBOBOX_OPTION: [] }] }, + { COMBOBOX_OPTION: [] }, + ], + }; + testAccessibleTree(dropdown, tree); + + // Collapse the select. Focus should return to the select. + focused = waitForEvent( + EVENT_FOCUS, + "select", + "select focused after collapsed" + ); + await invokeContentTask(browser, [], () => { + const { ContentTaskUtils } = ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeKey("VK_ESCAPE", {}, content); + }); + await focused; + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_table.js b/accessible/tests/browser/e10s/browser_treeupdate_table.js new file mode 100644 index 0000000000..5c2903225a --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_table.js @@ -0,0 +1,48 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + ` + <table id="table"> + <tr> + <td>cell1</td> + <td>cell2</td> + </tr> + </table>`, + async function(browser, accDoc) { + let table = findAccessibleChildByID(accDoc, "table"); + + let tree = { + TABLE: [ + { ROW: [{ CELL: [{ TEXT_LEAF: [] }] }, { CELL: [{ TEXT_LEAF: [] }] }] }, + ], + }; + testAccessibleTree(table, tree); + + let onReorder = waitForEvent(EVENT_REORDER, "table"); + await invokeContentTask(browser, [], () => { + // append a caption, it should appear as a first element in the + // accessible tree. + let doc = content.document; + let caption = doc.createElement("caption"); + caption.textContent = "table caption"; + doc.getElementById("table").appendChild(caption); + }); + await onReorder; + + tree = { + TABLE: [ + { CAPTION: [{ TEXT_LEAF: [] }] }, + { ROW: [{ CELL: [{ TEXT_LEAF: [] }] }, { CELL: [{ TEXT_LEAF: [] }] }] }, + ], + }; + testAccessibleTree(table, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js b/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js new file mode 100644 index 0000000000..6f89105b86 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_textleaf.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function removeTextData(browser, accessible, id, role) { + let tree = { + role, + children: [{ role: ROLE_TEXT_LEAF, name: "text" }], + }; + testAccessibleTree(accessible, tree); + + let onReorder = waitForEvent(EVENT_REORDER, id); + await invokeContentTask(browser, [id], contentId => { + content.document.getElementById(contentId).firstChild.textContent = ""; + }); + await onReorder; + + tree = { role, children: [] }; + testAccessibleTree(accessible, tree); +} + +addAccessibleTask( + ` + <p id="p">text</p> + <pre id="pre">text</pre>`, + async function(browser, accDoc) { + let p = findAccessibleChildByID(accDoc, "p"); + let pre = findAccessibleChildByID(accDoc, "pre"); + await removeTextData(browser, p, "p", ROLE_PARAGRAPH); + await removeTextData(browser, pre, "pre", ROLE_TEXT_CONTAINER); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_visibility.js b/accessible/tests/browser/e10s/browser_treeupdate_visibility.js new file mode 100644 index 0000000000..4583056586 --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_visibility.js @@ -0,0 +1,342 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function testTreeOnHide(browser, accDoc, containerID, id, before, after) { + let acc = findAccessibleChildByID(accDoc, containerID); + testAccessibleTree(acc, before); + + let onReorder = waitForEvent(EVENT_REORDER, containerID); + await invokeSetStyle(browser, id, "visibility", "hidden"); + await onReorder; + + testAccessibleTree(acc, after); +} + +async function test3(browser, accessible) { + let tree = { + SECTION: [ + // container + { + SECTION: [ + // parent + { + SECTION: [ + // child + { TEXT_LEAF: [] }, + ], + }, + ], + }, + { + SECTION: [ + // parent2 + { + SECTION: [ + // child2 + { TEXT_LEAF: [] }, + ], + }, + ], + }, + ], + }; + testAccessibleTree(accessible, tree); + + let onReorder = waitForEvent(EVENT_REORDER, "t3_container"); + await invokeContentTask(browser, [], () => { + let doc = content.document; + doc.getElementById("t3_container").style.color = "red"; + doc.getElementById("t3_parent").style.visibility = "hidden"; + doc.getElementById("t3_parent2").style.visibility = "hidden"; + }); + await onReorder; + + tree = { + SECTION: [ + // container + { + SECTION: [ + // child + { TEXT_LEAF: [] }, + ], + }, + { + SECTION: [ + // child2 + { TEXT_LEAF: [] }, + ], + }, + ], + }; + testAccessibleTree(accessible, tree); +} + +async function test4(browser, accessible) { + let tree = { + SECTION: [{ TABLE: [{ ROW: [{ CELL: [] }] }] }], + }; + testAccessibleTree(accessible, tree); + + let onReorder = waitForEvent(EVENT_REORDER, "t4_parent"); + await invokeContentTask(browser, [], () => { + let doc = content.document; + doc.getElementById("t4_container").style.color = "red"; + doc.getElementById("t4_child").style.visibility = "visible"; + }); + await onReorder; + + tree = { + SECTION: [ + { + TABLE: [ + { + ROW: [ + { + CELL: [ + { + SECTION: [ + { + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + testAccessibleTree(accessible, tree); +} + +addAccessibleTask( + "e10s/doc_treeupdate_visibility.html", + async function(browser, accDoc) { + let t3Container = findAccessibleChildByID(accDoc, "t3_container"); + let t4Container = findAccessibleChildByID(accDoc, "t4_container"); + + await testTreeOnHide( + browser, + accDoc, + "t1_container", + "t1_parent", + { + SECTION: [ + { + SECTION: [ + { + SECTION: [{ TEXT_LEAF: [] }], + }, + ], + }, + ], + }, + { + SECTION: [ + { + SECTION: [{ TEXT_LEAF: [] }], + }, + ], + } + ); + + await testTreeOnHide( + browser, + accDoc, + "t2_container", + "t2_grandparent", + { + SECTION: [ + { + // container + SECTION: [ + { + // grand parent + SECTION: [ + { + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + { + SECTION: [ + { + // child2 + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + SECTION: [ + { + // container + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + { + SECTION: [ + { + // child2 + TEXT_LEAF: [], + }, + ], + }, + ], + } + ); + + await test3(browser, t3Container); + await test4(browser, t4Container); + + await testTreeOnHide( + browser, + accDoc, + "t5_container", + "t5_subcontainer", + { + SECTION: [ + { + // container + SECTION: [ + { + // subcontainer + TABLE: [ + { + ROW: [ + { + CELL: [ + { + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + SECTION: [ + { + // container + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + ], + } + ); + + await testTreeOnHide( + browser, + accDoc, + "t6_container", + "t6_subcontainer", + { + SECTION: [ + { + // container + SECTION: [ + { + // subcontainer + TABLE: [ + { + ROW: [ + { + CELL: [ + { + TABLE: [ + { + // nested table + ROW: [ + { + CELL: [ + { + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + { + SECTION: [ + { + // child2 + TEXT_LEAF: [], + }, + ], + }, + ], + }, + ], + }, + { + SECTION: [ + { + // container + SECTION: [ + { + // child + TEXT_LEAF: [], + }, + ], + }, + { + SECTION: [ + { + // child2 + TEXT_LEAF: [], + }, + ], + }, + ], + } + ); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js new file mode 100644 index 0000000000..7f7e4ba97d --- /dev/null +++ b/accessible/tests/browser/e10s/browser_treeupdate_whitespace.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + "e10s/doc_treeupdate_whitespace.html", + async function(browser, accDoc) { + let container1 = findAccessibleChildByID(accDoc, "container1"); + let container2Parent = findAccessibleChildByID(accDoc, "container2-parent"); + + let tree = { + SECTION: [ + { GRAPHIC: [] }, + { TEXT_LEAF: [] }, + { GRAPHIC: [] }, + { TEXT_LEAF: [] }, + { GRAPHIC: [] }, + ], + }; + testAccessibleTree(container1, tree); + + let onReorder = waitForEvent(EVENT_REORDER, "container1"); + // Remove img1 from container1 + await invokeContentTask(browser, [], () => { + let doc = content.document; + doc.getElementById("container1").removeChild(doc.getElementById("img1")); + }); + await onReorder; + + tree = { + SECTION: [{ GRAPHIC: [] }, { TEXT_LEAF: [] }, { GRAPHIC: [] }], + }; + testAccessibleTree(container1, tree); + + tree = { + SECTION: [{ LINK: [] }, { LINK: [{ GRAPHIC: [] }] }], + }; + testAccessibleTree(container2Parent, tree); + + onReorder = waitForEvent(EVENT_REORDER, "container2-parent"); + // Append an img with valid src to container2 + await invokeContentTask(browser, [], () => { + let doc = content.document; + let img = doc.createElement("img"); + img.setAttribute( + "src", + "http://example.com/a11y/accessible/tests/mochitest/moz.png" + ); + doc.getElementById("container2").appendChild(img); + }); + await onReorder; + + tree = { + SECTION: [ + { LINK: [{ GRAPHIC: [] }] }, + { TEXT_LEAF: [] }, + { LINK: [{ GRAPHIC: [] }] }, + ], + }; + testAccessibleTree(container2Parent, tree); + }, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html b/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html new file mode 100644 index 0000000000..9d08854b9a --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_ariadialog.html @@ -0,0 +1,23 @@ +<html> + <head> + <meta charset="utf-8"/> + <title>Tree Update ARIA Dialog Test</title> + </head> + <body id="body"> + <div id="dialog" role="dialog" style="display: none;"> + <table id="table" role="presentation" + style="display: block; position: fixed; top: 88px; left: 312.5px; z-index: 10010;"> + <tbody> + <tr> + <td role="presentation"> + <div role="presentation"> + <a id="a" role="button">text</a> + </div> + <input id="input"> + </td> + </tr> + </tbody> + </table> + </div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html b/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html new file mode 100644 index 0000000000..38b5c333a1 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_ariaowns.html @@ -0,0 +1,44 @@ +<html> + <head> + <meta charset="utf-8"/> + <title>Tree Update ARIA Owns Test</title> + </head> + <body id="body"> + <div id="t1_container" aria-owns="t1_checkbox t1_button"> + <div role="button" id="t1_button"></div> + <div role="checkbox" id="t1_checkbox"> + <span id="t1_span"> + <div id="t1_subdiv"></div> + </span> + </div> + </div> + <div id="t1_group" role="group"></div> + <div id="t1_grouptmp" role="group"></div> + + <div id="t2_container1" aria-owns="t2_owned"></div> + <div id="t2_container2"> + <div id="t2_container3"><div id="t2_owned" role="checkbox"></div></div> + </div> + + <div id="t3_container1" aria-owns="t3_child"></div> + <div id="t3_child" role="checkbox"></div> + <div id="t3_container2"></div> + + <div id="t4_container1" aria-owns="t4_child1 t4_child2"></div> + <div id="t4_container2"> + <div id="t4_child1" style="display:none" role="checkbox"></div> + <div id="t4_child2" role="radio"></div> + </div> + + <div id="t5_container"> + <div role="button" id="t5_button"></div> + <div role="checkbox" id="t5_checkbox"></div> + <div role="radio" id="t5_radio"></div> + </div> + + <div id="t6_container" aria-owns="t6_fake"> + <span id="t6_span">hey</span> + </div> + <div id="t6_fake" role="group"></div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html b/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html new file mode 100644 index 0000000000..4dd230fc28 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_imagemap.html @@ -0,0 +1,21 @@ +<html> + <head> + <meta charset="utf-8"/> + <title>Tree Update Imagemap Test</title> + </head> + <body id="body"> + <map name="atoz_map" id="map"> + <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" alt="b" shape="rect"> + </map> + + <div id="container"> + <img id="imgmap" width="447" height="15" + usemap="#atoz_map" + src="http://example.com/a11y/accessible/tests/mochitest/letters.gif"><!-- + Important: no whitespace between the <img> and the </div>, so we + don't end up with textframes there, because those would be reflected + in our accessible tree in some cases. + --></div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml b/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml new file mode 100644 index 0000000000..9c59fb9d11 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_removal.xhtml @@ -0,0 +1,11 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf-8"/> + <title>Tree Update Removal Test</title> + </head> + <body id="body"> + <div id="the_displaynone" style="display: none;"></div> + <table id="the_table"></table> + <tr id="the_row"></tr> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_visibility.html b/accessible/tests/browser/e10s/doc_treeupdate_visibility.html new file mode 100644 index 0000000000..00213b2b70 --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_visibility.html @@ -0,0 +1,78 @@ +<html> + <head> + <meta charset="utf-8"/> + <title>Tree Update Visibility Test</title> + </head> + <body id="body"> + <!-- hide parent while child stays visible --> + <div id="t1_container"> + <div id="t1_parent"> + <div id="t1_child" style="visibility: visible">text</div> + </div> + </div> + + <!-- hide grandparent while its children stay visible --> + <div id="t2_container"> + <div id="t2_grandparent"> + <div id="t2_parent"> + <div id="t2_child" style="visibility: visible">text</div> + <div id="t2_child2" style="visibility: visible">text</div> + </div> + </div> + </div> + + <!-- change container style, hide parents while their children stay visible --> + <div id="t3_container"> + <div id="t3_parent"> + <div id="t3_child" style="visibility: visible">text</div> + </div> + <div id="t3_parent2"> + <div id="t3_child2" style="visibility: visible">text</div> + </div> + </div> + + <!-- change container style, show child inside the table --> + <div id="t4_container"> + <table> + <tr> + <td id="t4_parent"> + <div id="t4_child" style="visibility: hidden;">text</div> + </td> + </tr> + </table> + </div> + + <!-- hide subcontainer while child inside the table stays visible --> + <div id="t5_container"> + <div id="t5_subcontainer"> + <table> + <tr> + <td> + <div id="t5_child" style="visibility: visible;">text</div> + </td> + </tr> + </table> + </div> + </div> + + <!-- hide subcontainer while its child and child inside the nested table stays visible --> + <div id="t6_container"> + <div id="t6_subcontainer"> + <table> + <tr> + <td> + <table> + <tr> + <td> + <div id="t6_child" style="visibility: visible;">text</div> + </td> + </tr> + </table> + </td> + </tr> + </table> + <div id="t6_child2" style="visibility: visible">text</div> + </div> + </div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html new file mode 100644 index 0000000000..f17dbbd60e --- /dev/null +++ b/accessible/tests/browser/e10s/doc_treeupdate_whitespace.html @@ -0,0 +1,10 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf-8"/> + <title>Whitespace text accessible creation/destruction</title> + </head> + <body id="body"> + <div id="container1"> <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> <img id="img1" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> <img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> </div> + <div id="container2-parent"> <a id="container2"></a> <a><img src="http://example.com/a11y/accessible/tests/mochitest/moz.png"></a> </div> + </body> +</html> diff --git a/accessible/tests/browser/e10s/head.js b/accessible/tests/browser/e10s/head.js new file mode 100644 index 0000000000..672aa46171 --- /dev/null +++ b/accessible/tests/browser/e10s/head.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); diff --git a/accessible/tests/browser/events/browser.ini b/accessible/tests/browser/events/browser.ini new file mode 100644 index 0000000000..fe1d15917d --- /dev/null +++ b/accessible/tests/browser/events/browser.ini @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/mochitest/*.js + !/accessible/tests/browser/*.jsm + +[browser_test_docload.js] +skip-if = e10s +[browser_test_scrolling.js] +[browser_test_textcaret.js] +[browser_test_focus_browserui.js] +skip-if = sessionHistoryInParent +[browser_test_focus_dialog.js] +[browser_test_focus_urlbar.js] +[browser_test_A11yUtils_announce.js] +[browser_test_selection_urlbar.js] diff --git a/accessible/tests/browser/events/browser_test_A11yUtils_announce.js b/accessible/tests/browser/events/browser_test_A11yUtils_announce.js new file mode 100644 index 0000000000..b2848f35c2 --- /dev/null +++ b/accessible/tests/browser/events/browser_test_A11yUtils_announce.js @@ -0,0 +1,57 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +// Check that the browser A11yUtils.announce() function works correctly. +// Note that this does not use mozilla::a11y::Accessible::Announce and a11y +// announcement events, as these aren't yet supported on desktop. +async function runTests() { + const alert = document.getElementById("a11y-announcement"); + let alerted = waitForEvent(EVENT_ALERT, alert); + A11yUtils.announce({ raw: "first" }); + let event = await alerted; + const alertAcc = event.accessible; + is(alertAcc.role, ROLE_ALERT); + ok(!alertAcc.name); + is(alertAcc.childCount, 1); + is(alertAcc.firstChild.name, "first"); + + alerted = waitForEvent(EVENT_ALERT, alertAcc); + A11yUtils.announce({ raw: "second" }); + event = await alerted; + ok(!alertAcc.name); + is(alertAcc.childCount, 1); + is(alertAcc.firstChild.name, "second"); + + info("Testing Fluent message"); + // We need a simple Fluent message here without arguments or attributes. + const fluentId = "search-one-offs-with-title"; + const fluentMessage = await document.l10n.formatValue(fluentId); + alerted = waitForEvent(EVENT_ALERT, alertAcc); + A11yUtils.announce({ id: fluentId }); + event = await alerted; + ok(!alertAcc.name); + is(alertAcc.childCount, 1); + is(alertAcc.firstChild.name, fluentMessage); + + info("Ensuring Fluent message is cancelled if announce is re-entered"); + alerted = waitForEvent(EVENT_ALERT, alertAcc); + // This call runs async. + let asyncAnnounce = A11yUtils.announce({ id: fluentId }); + // Before the async call finishes, call announce again. + A11yUtils.announce({ raw: "third" }); + // Wait for the async call to complete. + await asyncAnnounce; + event = await alerted; + ok(!alertAcc.name); + is(alertAcc.childCount, 1); + // The async call should have been cancelled. If it wasn't, we would get + // fluentMessage here instead of "third". + is(alertAcc.firstChild.name, "third"); +} + +addAccessibleTask(``, runTests); diff --git a/accessible/tests/browser/events/browser_test_docload.js b/accessible/tests/browser/events/browser_test_docload.js new file mode 100644 index 0000000000..53722a582a --- /dev/null +++ b/accessible/tests/browser/events/browser_test_docload.js @@ -0,0 +1,120 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function busyChecker(isBusy) { + return function(event) { + let scEvent; + try { + scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + } catch (e) { + return false; + } + + return scEvent.state == STATE_BUSY && scEvent.isEnabled == isBusy; + }; +} + +function inIframeChecker(iframeId) { + return function(event) { + return getAccessibleDOMNodeID(event.accessibleDocument.parent) == iframeId; + }; +} + +function urlChecker(url) { + return function(event) { + info(`${event.accessibleDocument.URL} == ${url}`); + return event.accessibleDocument.URL == url; + }; +} + +async function runTests(browser, accDoc) { + let onLoadEvents = waitForEvents({ + expected: [ + [EVENT_REORDER, getAccessible(browser)], + [EVENT_DOCUMENT_LOAD_COMPLETE, "body2"], + [EVENT_STATE_CHANGE, busyChecker(false)], + ], + unexpected: [ + [EVENT_DOCUMENT_LOAD_COMPLETE, inIframeChecker("iframe1")], + [EVENT_STATE_CHANGE, inIframeChecker("iframe1")], + ], + }); + + BrowserTestUtils.loadURI( + browser, + `data:text/html;charset=utf-8, + <html><body id="body2"> + <iframe id="iframe1" src="http://example.com"></iframe> + </body></html>` + ); + + await onLoadEvents; + + onLoadEvents = waitForEvents([ + [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("about:about")], + [EVENT_STATE_CHANGE, busyChecker(false)], + [EVENT_REORDER, getAccessible(browser)], + ]); + + BrowserTestUtils.loadURI(browser, "about:about"); + + await onLoadEvents; + + onLoadEvents = waitForEvents([ + [EVENT_DOCUMENT_RELOAD, evt => evt.isFromUserInput], + [EVENT_REORDER, getAccessible(browser)], + [EVENT_STATE_CHANGE, busyChecker(false)], + ]); + + EventUtils.synthesizeKey("VK_F5", {}, browser.ownerGlobal); + + await onLoadEvents; + + onLoadEvents = waitForEvents([ + [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("about:mozilla")], + [EVENT_STATE_CHANGE, busyChecker(false)], + [EVENT_REORDER, getAccessible(browser)], + ]); + + BrowserTestUtils.loadURI(browser, "about:mozilla"); + + await onLoadEvents; + + onLoadEvents = waitForEvents([ + [EVENT_DOCUMENT_RELOAD, evt => !evt.isFromUserInput], + [EVENT_REORDER, getAccessible(browser)], + [EVENT_STATE_CHANGE, busyChecker(false)], + ]); + + browser.reload(); + + await onLoadEvents; + + onLoadEvents = waitForEvents([ + [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("http://www.wronguri.wronguri/")], + [EVENT_STATE_CHANGE, busyChecker(false)], + [EVENT_REORDER, getAccessible(browser)], + ]); + + BrowserTestUtils.loadURI(browser, "http://www.wronguri.wronguri/"); + + await onLoadEvents; + + onLoadEvents = waitForEvents([ + [EVENT_DOCUMENT_LOAD_COMPLETE, urlChecker("https://nocert.example.com/")], + [EVENT_STATE_CHANGE, busyChecker(false)], + [EVENT_REORDER, getAccessible(browser)], + ]); + + BrowserTestUtils.loadURI(browser, "https://nocert.example.com:443/"); + + await onLoadEvents; +} + +/** + * Test caching of accessible object states + */ +addAccessibleTask("", runTests); diff --git a/accessible/tests/browser/events/browser_test_focus_browserui.js b/accessible/tests/browser/events/browser_test_focus_browserui.js new file mode 100644 index 0000000000..98fc735269 --- /dev/null +++ b/accessible/tests/browser/events/browser_test_focus_browserui.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +/* import-globals-from ../../mochitest/role.js */ +loadScripts( + { name: "states.js", dir: MOCHITESTS_DIR }, + { name: "role.js", dir: MOCHITESTS_DIR } +); + +async function runTests(browser, accDoc) { + let onFocus = waitForEvent(EVENT_FOCUS, "input"); + EventUtils.synthesizeKey("VK_TAB", {}, browser.ownerGlobal); + let evt = await onFocus; + testStates(evt.accessible, STATE_FOCUSED); + + onFocus = waitForEvent(EVENT_FOCUS, "buttonInputDoc"); + let url = snippetToURL(`<input id="input" type="button" value="button">`, { + contentDocBodyAttrs: { id: "buttonInputDoc" }, + }); + browser.loadURI(url, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + evt = await onFocus; + testStates(evt.accessible, STATE_FOCUSED); + + onFocus = waitForEvent(EVENT_FOCUS, "input"); + browser.goBack(); + evt = await onFocus; + testStates(evt.accessible, STATE_FOCUSED); + + onFocus = waitForEvent( + EVENT_FOCUS, + event => event.accessible.DOMNode == gURLBar.inputField + ); + EventUtils.synthesizeKey("t", { accelKey: true }, browser.ownerGlobal); + evt = await onFocus; + testStates(evt.accessible, STATE_FOCUSED); + + onFocus = waitForEvent(EVENT_FOCUS, "input"); + EventUtils.synthesizeKey("w", { accelKey: true }, browser.ownerGlobal); + evt = await onFocus; + testStates(evt.accessible, STATE_FOCUSED); +} + +/** + * Accessibility loading document events test. + */ +addAccessibleTask(`<input id="input">`, runTests); diff --git a/accessible/tests/browser/events/browser_test_focus_dialog.js b/accessible/tests/browser/events/browser_test_focus_dialog.js new file mode 100644 index 0000000000..71485a678d --- /dev/null +++ b/accessible/tests/browser/events/browser_test_focus_dialog.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +/* import-globals-from ../../mochitest/role.js */ +loadScripts( + { name: "states.js", dir: MOCHITESTS_DIR }, + { name: "role.js", dir: MOCHITESTS_DIR } +); + +async function runTests(browser, accDoc) { + let onFocus = waitForEvent(EVENT_FOCUS, "button"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("button").focus(); + }); + let button = (await onFocus).accessible; + testStates(button, STATE_FOCUSED); + + // Bug 1377942 - The target of the focus event changes under different + // circumstances. + // In e10s the focus event is the new window, in non-e10s it's the doc. + onFocus = waitForEvent(EVENT_FOCUS, () => true); + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + // button should be blurred + await onFocus; + testStates(button, 0, 0, STATE_FOCUSED); + + onFocus = waitForEvent(EVENT_FOCUS, "button"); + await BrowserTestUtils.closeWindow(newWin); + testStates((await onFocus).accessible, STATE_FOCUSED); + + onFocus = waitForEvent(EVENT_FOCUS, "body2"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("editabledoc") + .contentWindow.document.body.focus(); + }); + testStates((await onFocus).accessible, STATE_FOCUSED); + + onFocus = waitForEvent(EVENT_FOCUS, "body2"); + newWin = await BrowserTestUtils.openNewBrowserWindow(); + await BrowserTestUtils.closeWindow(newWin); + testStates((await onFocus).accessible, STATE_FOCUSED); + + let onShow = waitForEvent(EVENT_SHOW, "alertdialog"); + onFocus = waitForEvent(EVENT_FOCUS, "alertdialog"); + await SpecialPowers.spawn(browser, [], () => { + let alertDialog = content.document.getElementById("alertdialog"); + alertDialog.style.display = "block"; + alertDialog.focus(); + }); + await onShow; + testStates((await onFocus).accessible, STATE_FOCUSED); +} + +/** + * Accessible dialog focus testing + */ +addAccessibleTask( + ` + <button id="button">button</button> + <iframe id="editabledoc" + src="${snippetToURL("", { + contentDocBodyAttrs: { id: "body2", contentEditable: "true" }, + })}"> + </iframe> + <div id="alertdialog" style="display: none" tabindex="-1" role="alertdialog" aria-labelledby="title2" aria-describedby="desc2"> + <div id="title2">Blah blah</div> + <div id="desc2">Woof woof woof.</div> + <button>Close</button> + </div>`, + runTests +); diff --git a/accessible/tests/browser/events/browser_test_focus_urlbar.js b/accessible/tests/browser/events/browser_test_focus_urlbar.js new file mode 100644 index 0000000000..874a0e239b --- /dev/null +++ b/accessible/tests/browser/events/browser_test_focus_urlbar.js @@ -0,0 +1,410 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +/* import-globals-from ../../mochitest/role.js */ +loadScripts( + { name: "states.js", dir: MOCHITESTS_DIR }, + { name: "role.js", dir: MOCHITESTS_DIR } +); + +XPCOMUtils.defineLazyModuleGetters(this, { + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm", + PlacesUtils: "resource://gre/modules/PlacesUtils.jsm", + UrlbarProvider: "resource:///modules/UrlbarUtils.jsm", + UrlbarProvidersManager: "resource:///modules/UrlbarProvidersManager.jsm", + UrlbarResult: "resource:///modules/UrlbarResult.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", + UrlbarUtils: "resource:///modules/UrlbarUtils.jsm", +}); + +function isEventForAutocompleteItem(event) { + return event.accessible.role == ROLE_COMBOBOX_OPTION; +} + +function isEventForButton(event) { + return event.accessible.role == ROLE_PUSHBUTTON; +} + +function isEventForOneOffEngine(event) { + let parent = event.accessible.parent; + return ( + event.accessible.role == ROLE_PUSHBUTTON && + parent && + parent.role == ROLE_GROUPING && + parent.name + ); +} + +function isEventForMenuPopup(event) { + return event.accessible.role == ROLE_MENUPOPUP; +} + +function isEventForMenuItem(event) { + return event.accessible.role == ROLE_MENUITEM; +} + +function isEventForTipButton(event) { + let parent = event.accessible.parent; + return ( + event.accessible.role == ROLE_PUSHBUTTON && + parent && + parent.role == ROLE_GROUPING && + parent.name + ); +} + +/** + * A test provider. + */ +class TipTestProvider extends UrlbarProvider { + constructor(matches) { + super(); + this._matches = matches; + } + get name() { + return "TipTestProvider"; + } + get type() { + return UrlbarUtils.PROVIDER_TYPE.PROFILE; + } + isActive(context) { + return true; + } + isRestricting(context) { + return true; + } + async startQuery(context, addCallback) { + this._context = context; + for (const match of this._matches) { + addCallback(this, match); + } + } +} + +// Check that the URL bar manages accessibility focus appropriately. +async function runTests() { + registerCleanupFunction(async function() { + await UrlbarTestUtils.promisePopupClose(window); + await PlacesUtils.history.clear(); + }); + + await PlacesTestUtils.addVisits([ + "http://example1.com/blah", + "http://example2.com/blah", + "http://example1.com/", + "http://example2.com/", + ]); + + // Ensure initial state. + await UrlbarTestUtils.promisePopupClose(window); + + let focused = waitForEvent( + EVENT_FOCUS, + event => event.accessible.role == ROLE_ENTRY + ); + gURLBar.focus(); + let event = await focused; + let textBox = event.accessible; + // Ensure the URL bar is ready for a new URL to be typed. + // Sometimes, when this test runs, the existing text isn't selected when the + // URL bar is focused. Pressing escape twice ensures that the popup is + // closed and that the existing text is selected. + EventUtils.synthesizeKey("KEY_Escape"); + EventUtils.synthesizeKey("KEY_Escape"); + + info("Ensuring no focus change when first text is typed"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "example", + fireInputEvent: true, + }); + // Wait a tick for a11y events to fire. + await TestUtils.waitForTick(); + testStates(textBox, STATE_FOCUSED); + + info("Ensuring no focus change on backspace"); + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + // Wait a tick for a11y events to fire. + await TestUtils.waitForTick(); + testStates(textBox, STATE_FOCUSED); + + info("Ensuring no focus change on text selection and delete"); + EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }); + EventUtils.synthesizeKey("KEY_Delete"); + await UrlbarTestUtils.promiseSearchComplete(window); + // Wait a tick for a11y events to fire. + await TestUtils.waitForTick(); + testStates(textBox, STATE_FOCUSED); + + info("Ensuring autocomplete focus on down arrow (1)"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring focus of another autocomplete item on down arrow"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring previous arrow selection state doesn't get stale on input"); + focused = waitForEvent(EVENT_FOCUS, textBox); + EventUtils.sendString("z"); + await focused; + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + testStates(textBox, STATE_FOCUSED); + + info("Ensuring focus of another autocomplete item on down arrow"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + if (AppConstants.platform == "macosx") { + info("Ensuring focus of another autocomplete item on ctrl-n"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("n", { ctrlKey: true }); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring focus of another autocomplete item on ctrl-p"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("p", { ctrlKey: true }); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + } + + info("Ensuring focus of another autocomplete item on up arrow"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowUp"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring text box focus on left arrow"); + focused = waitForEvent(EVENT_FOCUS, textBox); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + await focused; + testStates(textBox, STATE_FOCUSED); + + gURLBar.view.close(); + // On Mac, down arrow when not at the end of the field moves to the end. + // Move back to the end so the next press of down arrow opens the popup. + EventUtils.synthesizeKey("KEY_ArrowRight"); + + info("Ensuring autocomplete focus on down arrow (2)"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring autocomplete focus on arrow up for search settings button"); + focused = waitForEvent(EVENT_FOCUS, isEventForButton); + EventUtils.synthesizeKey("KEY_ArrowUp"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring text box focus when text is typed"); + focused = waitForEvent(EVENT_FOCUS, textBox); + EventUtils.sendString("z"); + await focused; + testStates(textBox, STATE_FOCUSED); + EventUtils.synthesizeKey("KEY_Backspace"); + await UrlbarTestUtils.promiseSearchComplete(window); + + info("Ensuring autocomplete focus on down arrow (3)"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring text box focus on backspace"); + focused = waitForEvent(EVENT_FOCUS, textBox); + EventUtils.synthesizeKey("KEY_Backspace"); + await focused; + testStates(textBox, STATE_FOCUSED); + await UrlbarTestUtils.promiseSearchComplete(window); + + info("Ensuring autocomplete focus on arrow down (4)"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + // Arrow down to the last result. + const resultCount = UrlbarTestUtils.getResultCount(window); + while (UrlbarTestUtils.getSelectedRowIndex(window) != resultCount - 1) { + EventUtils.synthesizeKey("KEY_ArrowDown"); + } + + info("Ensuring one-off search button focus on arrow down"); + focused = waitForEvent(EVENT_FOCUS, isEventForOneOffEngine); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring autocomplete focus on arrow up"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowUp"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring text box focus on text selection"); + focused = waitForEvent(EVENT_FOCUS, textBox); + EventUtils.synthesizeKey("KEY_ArrowLeft", { shiftKey: true }); + await focused; + testStates(textBox, STATE_FOCUSED); + + if (AppConstants.platform == "macosx") { + // On Mac, ctrl-n after arrow left/right does not re-open the popup. + // Type some text so the next press of ctrl-n opens the popup. + EventUtils.sendString("ple"); + + info("Ensuring autocomplete focus on ctrl-n"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("n", { ctrlKey: true }); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + } + + info( + "Ensuring context menu gets menu event on launch, item focus on down, and address bar focus on escape." + ); + let menuEvent = waitForEvent( + nsIAccessibleEvent.EVENT_MENUPOPUP_START, + isEventForMenuPopup + ); + await EventUtils.sendMouseEvent( + { type: "contextmenu" }, + gURLBar.querySelector("moz-input-box") + ); + await menuEvent; + + focused = waitForEvent(EVENT_FOCUS, isEventForMenuItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + focused = waitForEvent(EVENT_FOCUS, textBox); + let closed = waitForEvent( + nsIAccessibleEvent.EVENT_MENUPOPUP_END, + isEventForMenuPopup + ); + EventUtils.synthesizeKey("KEY_Escape"); + await closed; + await focused; + testStates(textBox, STATE_FOCUSED); +} + +// We test TIP results in their own test so the spoofed results don't interfere +// with the main test. +async function runTipTests() { + let matches = [ + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/a" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.TIP, + UrlbarUtils.RESULT_SOURCE.OTHER_LOCAL, + { + icon: "", + text: "This is a test intervention.", + buttonText: "Done", + type: "test", + helpUrl: "about:blank", + buttonUrl: "about:mozilla", + } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/b" } + ), + new UrlbarResult( + UrlbarUtils.RESULT_TYPE.URL, + UrlbarUtils.RESULT_SOURCE.HISTORY, + { url: "http://mozilla.org/c" } + ), + ]; + + // Ensure the tip appears in the expected position. + matches[1].suggestedIndex = 2; + + let provider = new TipTestProvider(matches); + UrlbarProvidersManager.registerProvider(provider); + + registerCleanupFunction(async function() { + UrlbarProvidersManager.unregisterProvider(provider); + }); + + let focused = waitForEvent( + EVENT_FOCUS, + event => event.accessible.role == ROLE_ENTRY + ); + gURLBar.focus(); + let event = await focused; + let textBox = event.accessible; + + EventUtils.synthesizeKey("KEY_Escape"); + EventUtils.synthesizeKey("KEY_Escape"); + + info("Ensuring no focus change when first text is typed"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "example", + fireInputEvent: true, + }); + // Wait a tick for a11y events to fire. + await TestUtils.waitForTick(); + testStates(textBox, STATE_FOCUSED); + + info("Ensuring autocomplete focus on down arrow (1)"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring the tip button is focused on down arrow"); + info("Also ensuring that the tip button is a part of a labelled group"); + focused = waitForEvent(EVENT_FOCUS, isEventForTipButton); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring the help button is focused on down arrow"); + info("Also ensuring that the help button is a part of a labelled group"); + focused = waitForEvent(EVENT_FOCUS, isEventForTipButton); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring autocomplete focus on down arrow (2)"); + focused = waitForEvent(EVENT_FOCUS, isEventForAutocompleteItem); + EventUtils.synthesizeKey("KEY_ArrowDown"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring the help button is focused on up arrow"); + focused = waitForEvent(EVENT_FOCUS, isEventForTipButton); + EventUtils.synthesizeKey("KEY_ArrowUp"); + event = await focused; + testStates(event.accessible, STATE_FOCUSED); + + info("Ensuring text box focus on left arrow, and not back to the tip button"); + focused = waitForEvent(EVENT_FOCUS, textBox); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + await focused; + testStates(textBox, STATE_FOCUSED); +} + +addAccessibleTask(``, runTests); +addAccessibleTask(``, runTipTests); diff --git a/accessible/tests/browser/events/browser_test_scrolling.js b/accessible/tests/browser/events/browser_test_scrolling.js new file mode 100644 index 0000000000..d9a4a0a73f --- /dev/null +++ b/accessible/tests/browser/events/browser_test_scrolling.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +addAccessibleTask( + ` + <div style="height: 100vh" id="one">one</div> + <div style="height: 100vh" id="two">two</div> + <div style="height: 100vh; width: 200vw; overflow: auto;" id="three"> + <div style="height: 300%;">three</div> + </div>`, + async function(browser, accDoc) { + let onScrolling = waitForEvents([ + [EVENT_SCROLLING, accDoc], + [EVENT_SCROLLING_END, accDoc], + ]); + await SpecialPowers.spawn(browser, [], () => { + content.location.hash = "#two"; + }); + let [scrollEvent1, scrollEndEvent1] = await onScrolling; + scrollEvent1.QueryInterface(nsIAccessibleScrollingEvent); + ok( + scrollEvent1.maxScrollY >= scrollEvent1.scrollY, + "scrollY is within max" + ); + scrollEndEvent1.QueryInterface(nsIAccessibleScrollingEvent); + ok( + scrollEndEvent1.maxScrollY >= scrollEndEvent1.scrollY, + "scrollY is within max" + ); + + onScrolling = waitForEvents([ + [EVENT_SCROLLING, accDoc], + [EVENT_SCROLLING_END, accDoc], + ]); + await SpecialPowers.spawn(browser, [], () => { + content.location.hash = "#three"; + }); + let [scrollEvent2, scrollEndEvent2] = await onScrolling; + scrollEvent2.QueryInterface(nsIAccessibleScrollingEvent); + ok( + scrollEvent2.scrollY > scrollEvent1.scrollY, + `${scrollEvent2.scrollY} > ${scrollEvent1.scrollY}` + ); + scrollEndEvent2.QueryInterface(nsIAccessibleScrollingEvent); + ok( + scrollEndEvent2.maxScrollY >= scrollEndEvent2.scrollY, + "scrollY is within max" + ); + + onScrolling = waitForEvents([ + [EVENT_SCROLLING, accDoc], + [EVENT_SCROLLING_END, accDoc], + ]); + await SpecialPowers.spawn(browser, [], () => { + content.scrollTo(10, 0); + }); + let [scrollEvent3, scrollEndEvent3] = await onScrolling; + scrollEvent3.QueryInterface(nsIAccessibleScrollingEvent); + ok( + scrollEvent3.maxScrollX >= scrollEvent3.scrollX, + "scrollX is within max" + ); + scrollEndEvent3.QueryInterface(nsIAccessibleScrollingEvent); + ok( + scrollEndEvent3.maxScrollX >= scrollEndEvent3.scrollX, + "scrollY is within max" + ); + ok( + scrollEvent3.scrollX > scrollEvent2.scrollX, + `${scrollEvent3.scrollX} > ${scrollEvent2.scrollX}` + ); + + // non-doc scrolling + onScrolling = waitForEvents([ + [EVENT_SCROLLING, "three"], + [EVENT_SCROLLING_END, "three"], + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document.querySelector("#three").scrollTo(0, 10); + }); + let [scrollEvent4, scrollEndEvent4] = await onScrolling; + scrollEvent4.QueryInterface(nsIAccessibleScrollingEvent); + ok( + scrollEvent4.maxScrollY >= scrollEvent4.scrollY, + "scrollY is within max" + ); + scrollEndEvent4.QueryInterface(nsIAccessibleScrollingEvent); + ok( + scrollEndEvent4.maxScrollY >= scrollEndEvent4.scrollY, + "scrollY is within max" + ); + } +); diff --git a/accessible/tests/browser/events/browser_test_selection_urlbar.js b/accessible/tests/browser/events/browser_test_selection_urlbar.js new file mode 100644 index 0000000000..0a773ce896 --- /dev/null +++ b/accessible/tests/browser/events/browser_test_selection_urlbar.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +XPCOMUtils.defineLazyModuleGetters(this, { + BrowserTestUtils: "resource://testing-common/BrowserTestUtils.jsm", + PlacesTestUtils: "resource://testing-common/PlacesTestUtils.jsm", + UrlbarTestUtils: "resource://testing-common/UrlbarTestUtils.jsm", +}); + +// Check that the URL bar manages accessibility +// selection notifications appropriately on startup (new window). +async function runTests() { + let focused = waitForEvent( + EVENT_FOCUS, + event => event.accessible.role == ROLE_ENTRY + ); + info("Creating new window"); + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + await PlacesTestUtils.addVisits("http://addons.mozilla.org"); + + registerCleanupFunction(async function() { + await BrowserTestUtils.closeWindow(newWin); + await PlacesUtils.history.clear(); + }); + info("Focusing window"); + newWin.focus(); + await focused; + + // Ensure the URL bar is ready for a new URL to be typed. + // Sometimes, when this test runs, the existing text isn't selected when the + // URL bar is focused. Pressing escape twice ensures that the popup is + // closed and that the existing text is selected. + EventUtils.synthesizeKey("KEY_Escape", {}, newWin); + EventUtils.synthesizeKey("KEY_Escape", {}, newWin); + let caretMoved = waitForEvent( + EVENT_TEXT_CARET_MOVED, + event => event.accessible.role == ROLE_ENTRY + ); + + info("Autofilling after typing `a` in new window URL bar."); + EventUtils.synthesizeKey("a", {}, newWin); + await UrlbarTestUtils.promiseSearchComplete(newWin); + Assert.equal( + newWin.gURLBar.inputField.value, + "addons.mozilla.org/", + "autofilled value as expected" + ); + + info("Ensuring caret moved on text selection"); + await caretMoved; +} + +addAccessibleTask(``, runTests); diff --git a/accessible/tests/browser/events/browser_test_textcaret.js b/accessible/tests/browser/events/browser_test_textcaret.js new file mode 100644 index 0000000000..d4e0f11a0f --- /dev/null +++ b/accessible/tests/browser/events/browser_test_textcaret.js @@ -0,0 +1,58 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Caret move events checker. + */ +function caretMoveChecker(target, caretOffset) { + return function(event) { + let cmEvent = event.QueryInterface(nsIAccessibleCaretMoveEvent); + return ( + cmEvent.accessible == getAccessible(target) && + cmEvent.caretOffset == caretOffset + ); + }; +} + +async function checkURLBarCaretEvents() { + const kURL = "about:mozilla"; + let newWin = await BrowserTestUtils.openNewBrowserWindow(); + BrowserTestUtils.loadURI(newWin.gBrowser.selectedBrowser, kURL); + newWin.gBrowser.selectedBrowser.focus(); + + await waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event => { + try { + return event.accessible.QueryInterface(nsIAccessibleDocument).URL == kURL; + } catch (e) { + return false; + } + }); + info("Loaded " + kURL); + + let urlbarInputEl = newWin.gURLBar.inputField; + let urlbarInput = getAccessible(urlbarInputEl, [nsIAccessibleText]); + + let onCaretMove = waitForEvents([ + [EVENT_TEXT_CARET_MOVED, caretMoveChecker(urlbarInput, kURL.length)], + [EVENT_FOCUS, urlbarInput], + ]); + + urlbarInput.caretOffset = -1; + await onCaretMove; + ok(true, "Caret move in URL bar #1"); + + onCaretMove = waitForEvent( + EVENT_TEXT_CARET_MOVED, + caretMoveChecker(urlbarInput, 0) + ); + + urlbarInput.caretOffset = 0; + await onCaretMove; + ok(true, "Caret move in URL bar #2"); + + await BrowserTestUtils.closeWindow(newWin); +} + +add_task(checkURLBarCaretEvents); diff --git a/accessible/tests/browser/events/head.js b/accessible/tests/browser/events/head.js new file mode 100644 index 0000000000..672aa46171 --- /dev/null +++ b/accessible/tests/browser/events/head.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); diff --git a/accessible/tests/browser/fission/browser.ini b/accessible/tests/browser/fission/browser.ini new file mode 100644 index 0000000000..a3fc7aa839 --- /dev/null +++ b/accessible/tests/browser/fission/browser.ini @@ -0,0 +1,14 @@ +[DEFAULT] +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js + +[browser_content_tree.js] +[browser_hidden_iframe.js] +[browser_nested_iframe.js] +[browser_reframe_root.js] +[browser_reframe_visibility.js] +[browser_src_change.js] +[browser_take_focus.js] diff --git a/accessible/tests/browser/fission/browser_content_tree.js b/accessible/tests/browser/fission/browser_content_tree.js new file mode 100644 index 0000000000..54df06c7f4 --- /dev/null +++ b/accessible/tests/browser/fission/browser_content_tree.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + `<table id="table"> + <tr> + <td>cell1</td> + <td>cell2</td> + </tr> + </table> + <ul id="ul"> + <li id="li">item1</li> + </ul>`, + async function(browser, iframeDocAcc, contentDocAcc) { + ok(iframeDocAcc, "IFRAME document accessible is present"); + (gIsRemoteIframe ? isnot : is)( + browser.browsingContext.currentWindowGlobal.osPid, + browser.browsingContext.children[0].currentWindowGlobal.osPid, + `Content and IFRAME documents are in ${ + gIsRemoteIframe ? "separate processes" : "same process" + }.` + ); + + const tree = { + DOCUMENT: [ + { + INTERNAL_FRAME: [ + { + DOCUMENT: [ + { + TABLE: [ + { + ROW: [ + { CELL: [{ TEXT_LEAF: [] }] }, + { CELL: [{ TEXT_LEAF: [] }] }, + ], + }, + ], + }, + { + LIST: [ + { + LISTITEM: [{ LISTITEM_MARKER: [] }, { TEXT_LEAF: [] }], + }, + ], + }, + ], + }, + ], + }, + ], + }; + testAccessibleTree(contentDocAcc, tree); + + const iframeAcc = contentDocAcc.getChildAt(0); + is( + iframeAcc.getChildAt(0), + iframeDocAcc, + "Document for the IFRAME matches IFRAME's first child." + ); + + is( + iframeDocAcc.parent, + iframeAcc, + "IFRAME document's parent matches the IFRAME." + ); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/fission/browser_hidden_iframe.js b/accessible/tests/browser/fission/browser_hidden_iframe.js new file mode 100644 index 0000000000..94b3015424 --- /dev/null +++ b/accessible/tests/browser/fission/browser_hidden_iframe.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "states.js", dir: MOCHITESTS_DIR }); +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + `<input id="textbox" value="hello"/>`, + async function(browser, contentDocAcc) { + info( + "Check that the IFRAME and the IFRAME document are not accessible initially." + ); + let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID); + let iframeDocAcc = findAccessibleChildByID( + contentDocAcc, + DEFAULT_IFRAME_DOC_BODY_ID + ); + ok(!iframeAcc, "IFRAME is hidden and should not be accessible"); + ok(!iframeDocAcc, "IFRAME document is hidden and should be accessible"); + + info( + "Show the IFRAME and check that it's now available in the accessibility tree." + ); + + const events = [[EVENT_REORDER, contentDocAcc]]; + // Until this event is fired, IFRAME accessible has no children attached. + events.push([ + EVENT_STATE_CHANGE, + event => { + const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + const id = getAccessibleDOMNodeID(event.accessible); + return ( + id === DEFAULT_IFRAME_DOC_BODY_ID && + scEvent.state === STATE_BUSY && + scEvent.isEnabled === false + ); + }, + ]); + + const onEvents = waitForEvents(events); + await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => { + content.document.getElementById(contentId).style.display = ""; + }); + await onEvents; + + iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID); + iframeDocAcc = findAccessibleChildByID( + contentDocAcc, + DEFAULT_IFRAME_DOC_BODY_ID + ); + + ok(!isDefunct(iframeAcc), "IFRAME should be accessible"); + is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child"); + ok(iframeDocAcc, "IFRAME document exists"); + ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible"); + is( + iframeAcc.firstChild, + iframeDocAcc, + "An accessible for a IFRAME document is the child of the IFRAME accessible" + ); + is( + iframeDocAcc.parent, + iframeAcc, + "IFRAME document's parent matches the IFRAME." + ); + }, + { + topLevel: false, + iframe: true, + remoteIframe: true, + iframeAttrs: { + style: "display: none;", + }, + skipFissionDocLoad: true, + } +); diff --git a/accessible/tests/browser/fission/browser_nested_iframe.js b/accessible/tests/browser/fission/browser_nested_iframe.js new file mode 100644 index 0000000000..0e48d767f8 --- /dev/null +++ b/accessible/tests/browser/fission/browser_nested_iframe.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +const NESTED_IFRAME_DOC_BODY_ID = "nested-iframe-body"; +const NESTED_IFRAME_ID = "nested-iframe"; +const nestedURL = new URL(`http://example.com/document-builder.sjs`); +nestedURL.searchParams.append( + "html", + `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Nested Iframe Frame Test</title> + </head> + <body id="${NESTED_IFRAME_DOC_BODY_ID}"> + <table id="table"> + <tr> + <td>cell1</td> + <td>cell2</td> + </tr> + </table> + <ul id="ul"> + <li id="li">item1</li> + </ul> + </body> + </html>` +); + +function getOsPid(browsingContext) { + return browsingContext.currentWindowGlobal.osPid; +} + +addAccessibleTask( + `<iframe id="${NESTED_IFRAME_ID}" src="${nestedURL.href}"/>`, + async function(browser, iframeDocAcc, contentDocAcc) { + let nestedDocAcc = findAccessibleChildByID( + iframeDocAcc, + NESTED_IFRAME_DOC_BODY_ID + ); + + ok(iframeDocAcc, "IFRAME document accessible is present"); + ok(nestedDocAcc, "Nested IFRAME document accessible is present"); + + const state = {}; + nestedDocAcc.getState(state, {}); + if (state.value & STATE_BUSY) { + nestedDocAcc = ( + await waitForEvent( + EVENT_DOCUMENT_LOAD_COMPLETE, + NESTED_IFRAME_DOC_BODY_ID + ) + ).accessible; + } + + if (gIsRemoteIframe) { + isnot( + getOsPid(browser.browsingContext), + getOsPid(browser.browsingContext.children[0]), + `Content and IFRAME documents are in separate processes.` + ); + isnot( + getOsPid(browser.browsingContext), + getOsPid(browser.browsingContext.children[0].children[0]), + `Content and nested IFRAME documents are in separate processes.` + ); + isnot( + getOsPid(browser.browsingContext.children[0]), + getOsPid(browser.browsingContext.children[0].children[0]), + `IFRAME and nested IFRAME documents are in separate processes.` + ); + } else { + is( + getOsPid(browser.browsingContext), + getOsPid(browser.browsingContext.children[0]), + `Content and IFRAME documents are in same processes.` + ); + if (gFissionBrowser) { + isnot( + getOsPid(browser.browsingContext.children[0]), + getOsPid(browser.browsingContext.children[0].children[0]), + `IFRAME and nested IFRAME documents are in separate processes.` + ); + } else { + is( + getOsPid(browser.browsingContext), + getOsPid(browser.browsingContext.children[0].children[0]), + `Content and nested IFRAME documents are in same processes.` + ); + } + } + + const tree = { + DOCUMENT: [ + { + INTERNAL_FRAME: [ + { + DOCUMENT: [ + { + INTERNAL_FRAME: [ + { + DOCUMENT: [ + { + TABLE: [ + { + ROW: [ + { CELL: [{ TEXT_LEAF: [] }] }, + { CELL: [{ TEXT_LEAF: [] }] }, + ], + }, + ], + }, + { + LIST: [ + { + LISTITEM: [ + { LISTITEM_MARKER: [] }, + { TEXT_LEAF: [] }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + testAccessibleTree(contentDocAcc, tree); + + const nestedIframeAcc = iframeDocAcc.getChildAt(0); + is( + nestedIframeAcc.getChildAt(0), + nestedDocAcc, + "Document for nested IFRAME matches." + ); + + is( + nestedDocAcc.parent, + nestedIframeAcc, + "Nested IFRAME document's parent matches the nested IFRAME." + ); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/fission/browser_reframe_root.js b/accessible/tests/browser/fission/browser_reframe_root.js new file mode 100644 index 0000000000..66dcf249bf --- /dev/null +++ b/accessible/tests/browser/fission/browser_reframe_root.js @@ -0,0 +1,95 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +/* import-globals-from ../../mochitest/role.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +addAccessibleTask( + `<input id="textbox" value="hello"/>`, + async function(browser, iframeDocAcc, contentDocAcc) { + info( + "Check that the IFRAME and the IFRAME document are accessible initially." + ); + let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID); + ok(!isDefunct(iframeAcc), "IFRAME should be accessible"); + ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible"); + + info("Move the IFRAME under a new hidden root."); + let onEvents = waitForEvent(EVENT_REORDER, contentDocAcc); + await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], id => { + const doc = content.document; + const root = doc.createElement("div"); + root.style.display = "none"; + doc.body.appendChild(root); + root.appendChild(doc.getElementById(id)); + }); + await onEvents; + + ok( + isDefunct(iframeAcc), + "IFRAME accessible should be defunct when hidden." + ); + ok( + isDefunct(iframeDocAcc), + "IFRAME document's accessible should be defunct when the IFRAME is hidden." + ); + ok( + !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID), + "No accessible for an IFRAME present." + ); + ok( + !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_DOC_BODY_ID), + "No accessible for the IFRAME document present." + ); + + info("Move the IFRAME back under the content document's body."); + onEvents = waitForEvents([ + [EVENT_REORDER, contentDocAcc], + [ + EVENT_STATE_CHANGE, + event => { + const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + const id = getAccessibleDOMNodeID(event.accessible); + return ( + id === DEFAULT_IFRAME_DOC_BODY_ID && + scEvent.state === STATE_BUSY && + scEvent.isEnabled === false + ); + }, + ], + ]); + await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], id => { + content.document.body.appendChild(content.document.getElementById(id)); + }); + await onEvents; + + iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID); + const newiframeDocAcc = iframeAcc.firstChild; + + ok(!isDefunct(iframeAcc), "IFRAME should be accessible"); + is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child"); + ok(!isDefunct(newiframeDocAcc), "IFRAME document should be accessible"); + ok( + isDefunct(iframeDocAcc), + "Original IFRAME document accessible should be defunct." + ); + isnot( + iframeAcc.firstChild, + iframeDocAcc, + "A new accessible is created for a IFRAME document." + ); + is( + iframeAcc.firstChild, + newiframeDocAcc, + "A new accessible for a IFRAME document is the child of the IFRAME accessible" + ); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/fission/browser_reframe_visibility.js b/accessible/tests/browser/fission/browser_reframe_visibility.js new file mode 100644 index 0000000000..bddb651f91 --- /dev/null +++ b/accessible/tests/browser/fission/browser_reframe_visibility.js @@ -0,0 +1,116 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "states.js", dir: MOCHITESTS_DIR }); +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + `<input id="textbox" value="hello"/>`, + async function(browser, iframeDocAcc, contentDocAcc) { + info( + "Check that the IFRAME and the IFRAME document are accessible initially." + ); + let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID); + ok(!isDefunct(iframeAcc), "IFRAME should be accessible"); + ok(!isDefunct(iframeDocAcc), "IFRAME document should be accessible"); + + info( + "Hide the IFRAME and check that it's gone along with the IFRAME document." + ); + let onEvents = waitForEvent(EVENT_REORDER, contentDocAcc); + await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => { + content.document.getElementById(contentId).style.display = "none"; + }); + await onEvents; + + ok( + isDefunct(iframeAcc), + "IFRAME accessible should be defunct when hidden." + ); + if (gIsRemoteIframe) { + ok( + !isDefunct(iframeDocAcc), + "IFRAME document's accessible is not defunct when the IFRAME is hidden and fission is enabled." + ); + } else { + ok( + isDefunct(iframeDocAcc), + "IFRAME document's accessible is defunct when the IFRAME is hidden and fission is not enabled." + ); + } + ok( + !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID), + "No accessible for an IFRAME present." + ); + ok( + !findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_DOC_BODY_ID), + "No accessible for the IFRAME document present." + ); + + info( + "Show the IFRAME and check that a new accessible is created for it as " + + "well as the IFRAME document." + ); + + const events = [[EVENT_REORDER, contentDocAcc]]; + if (!gIsRemoteIframe) { + events.push([ + EVENT_STATE_CHANGE, + event => { + const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + const id = getAccessibleDOMNodeID(event.accessible); + return ( + id === DEFAULT_IFRAME_DOC_BODY_ID && + scEvent.state === STATE_BUSY && + scEvent.isEnabled === false + ); + }, + ]); + } + onEvents = waitForEvents(events); + await SpecialPowers.spawn(browser, [DEFAULT_IFRAME_ID], contentId => { + content.document.getElementById(contentId).style.display = "block"; + }); + await onEvents; + + iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID); + const newiframeDocAcc = iframeAcc.firstChild; + + ok(!isDefunct(iframeAcc), "IFRAME should be accessible"); + is(iframeAcc.childCount, 1, "IFRAME accessible should have a single child"); + ok(newiframeDocAcc, "IFRAME document exists"); + ok(!isDefunct(newiframeDocAcc), "IFRAME document should be accessible"); + if (gIsRemoteIframe) { + ok( + !isDefunct(iframeDocAcc), + "Original IFRAME document accessible should not be defunct when fission is enabled." + ); + is( + iframeAcc.firstChild, + iframeDocAcc, + "Existing accessible is used for a IFRAME document." + ); + } else { + ok( + isDefunct(iframeDocAcc), + "Original IFRAME document accessible should be defunct when fission is not enabled." + ); + isnot( + iframeAcc.firstChild, + iframeDocAcc, + "A new accessible is created for a IFRAME document." + ); + } + is( + iframeAcc.firstChild, + newiframeDocAcc, + "A new accessible for a IFRAME document is the child of the IFRAME accessible" + ); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/fission/browser_src_change.js b/accessible/tests/browser/fission/browser_src_change.js new file mode 100644 index 0000000000..c490468b15 --- /dev/null +++ b/accessible/tests/browser/fission/browser_src_change.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + `<input id="textbox" value="hello"/>`, + async function(browser, iframeDocAcc, contentDocAcc) { + info( + "Check that the IFRAME and the IFRAME document are accessible initially." + ); + let iframeAcc = findAccessibleChildByID(contentDocAcc, DEFAULT_IFRAME_ID); + ok(isAccessible(iframeAcc), "IFRAME should be accessible"); + ok(isAccessible(iframeDocAcc), "IFRAME document should be accessible"); + + info("Replace src URL for the IFRAME with one with different origin."); + const onDocLoad = waitForEvent( + EVENT_DOCUMENT_LOAD_COMPLETE, + DEFAULT_IFRAME_DOC_BODY_ID + ); + + await SpecialPowers.spawn( + browser, + [DEFAULT_IFRAME_ID, CURRENT_CONTENT_DIR], + (id, olddir) => { + const { src } = content.document.getElementById(id); + content.document.getElementById(id).src = src.replace( + olddir, + "http://example.net/browser/accessible/tests/browser/" + ); + } + ); + const newiframeDocAcc = (await onDocLoad).accessible; + + ok(isAccessible(iframeAcc), "IFRAME should be accessible"); + ok( + isAccessible(newiframeDocAcc), + "new IFRAME document should be accessible" + ); + isnot( + iframeDocAcc, + newiframeDocAcc, + "A new accessible is created for a IFRAME document." + ); + is( + iframeAcc.firstChild, + newiframeDocAcc, + "An IFRAME has a new accessible for a IFRAME document as a child." + ); + is( + newiframeDocAcc.parent, + iframeAcc, + "A new accessible for a IFRAME document has an IFRAME as a parent." + ); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/fission/browser_take_focus.js b/accessible/tests/browser/fission/browser_take_focus.js new file mode 100644 index 0000000000..6e6eba662e --- /dev/null +++ b/accessible/tests/browser/fission/browser_take_focus.js @@ -0,0 +1,26 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +addAccessibleTask( + `<input id="textbox" value="hello"/>`, + async function(browser, iframeDocAcc, contentDocAcc) { + const textbox = findAccessibleChildByID(iframeDocAcc, "textbox"); + testStates(textbox, STATE_FOCUSABLE, 0, STATE_FOCUSED); + + let onFocus = waitForEvent(EVENT_FOCUS, textbox); + textbox.takeFocus(); + await onFocus; + + testStates(textbox, STATE_FOCUSABLE | STATE_FOCUSED, 0); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/fission/head.js b/accessible/tests/browser/fission/head.js new file mode 100644 index 0000000000..672aa46171 --- /dev/null +++ b/accessible/tests/browser/fission/head.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); diff --git a/accessible/tests/browser/general/browser.ini b/accessible/tests/browser/general/browser.ini new file mode 100644 index 0000000000..93ff094995 --- /dev/null +++ b/accessible/tests/browser/general/browser.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + !/accessible/tests/browser/shared-head.js + head.js + !/accessible/tests/mochitest/*.js +skip-if = a11y_checks || (os == 'win' && processor == 'aarch64') # windows-aarch64, 1534811 + +[browser_test_doc_creation.js] +[browser_test_urlbar.js] diff --git a/accessible/tests/browser/general/browser_test_doc_creation.js b/accessible/tests/browser/general/browser_test_doc_creation.js new file mode 100644 index 0000000000..7ee07f63fd --- /dev/null +++ b/accessible/tests/browser/general/browser_test_doc_creation.js @@ -0,0 +1,55 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const tab1URL = `data:text/html, + <html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf-8"/> + <title>First tab to be loaded</title> + </head> + <body> + <butotn>JUST A BUTTON</butotn> + </body> + </html>`; + +const tab2URL = `data:text/html, + <html xmlns="http://www.w3.org/1999/xhtml"> + <head> + <meta charset="utf-8"/> + <title>Second tab to be loaded</title> + </head> + <body> + <butotn>JUST A BUTTON</butotn> + </body> + </html>`; + +// Checking that, if there are open windows before accessibility was started, +// root accessibles for open windows are created so that all root accessibles +// are stored in application accessible children array. +add_task(async function testDocumentCreation() { + let tab1 = await openNewTab(tab1URL); + let tab2 = await openNewTab(tab2URL); + let accService = await initAccessibilityService(); + + info("Verifying that each tab content document is in accessible cache."); + for (const browser of [...gBrowser.browsers]) { + await SpecialPowers.spawn(browser, [], async () => { + let accServiceContent = Cc[ + "@mozilla.org/accessibilityService;1" + ].getService(Ci.nsIAccessibilityService); + Assert.ok( + !!accServiceContent.getAccessibleFromCache(content.document), + "Document accessible is in cache." + ); + }); + } + + BrowserTestUtils.removeTab(tab1); + BrowserTestUtils.removeTab(tab2); + + accService = null; // eslint-disable-line no-unused-vars + await shutdownAccessibilityService(); +}); diff --git a/accessible/tests/browser/general/browser_test_urlbar.js b/accessible/tests/browser/general/browser_test_urlbar.js new file mode 100644 index 0000000000..8c0c7797b5 --- /dev/null +++ b/accessible/tests/browser/general/browser_test_urlbar.js @@ -0,0 +1,40 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { UrlbarTestUtils } = ChromeUtils.import( + "resource://testing-common/UrlbarTestUtils.jsm" +); + +// Checking that the awesomebar popup gets COMBOBOX_LIST role instead of +// LISTBOX, since its parent is a <panel> (see Bug 1422465) +add_task(async function testAutocompleteRichResult() { + let tab = await openNewTab("data:text/html;charset=utf-8,"); + let accService = await initAccessibilityService(); + + info("Opening the URL bar and entering a key to show the urlbar panel"); + await UrlbarTestUtils.promiseAutocompleteResultPopup({ + window, + waitForFocus, + value: "a", + }); + + info("Waiting for accessibility to be created for the results list"); + let resultsView; + resultsView = gURLBar.view.panel.querySelector(".urlbarView-results"); + await TestUtils.waitForCondition(() => + accService.getAccessibleFor(resultsView) + ); + + info("Confirming that the special case is handled in XULListboxAccessible"); + let accessible = accService.getAccessibleFor(resultsView); + is(accessible.role, ROLE_COMBOBOX_LIST, "Right role"); + + BrowserTestUtils.removeTab(tab); +}); + +registerCleanupFunction(async function() { + await shutdownAccessibilityService(); +}); diff --git a/accessible/tests/browser/general/head.js b/accessible/tests/browser/general/head.js new file mode 100644 index 0000000000..cd03a441f3 --- /dev/null +++ b/accessible/tests/browser/general/head.js @@ -0,0 +1,72 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* exported initAccessibilityService, openNewTab, shutdownAccessibilityService */ + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +const nsIAccessibleRole = Ci.nsIAccessibleRole; // eslint-disable-line no-unused-vars + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +async function openNewTab(url) { + const forceNewProcess = true; + + return BrowserTestUtils.openNewForegroundTab({ + gBrowser, + url, + forceNewProcess, + }); +} + +async function initAccessibilityService() { + info("Create accessibility service."); + let accService = Cc["@mozilla.org/accessibilityService;1"].getService( + Ci.nsIAccessibilityService + ); + + await new Promise(resolve => { + if (Services.appinfo.accessibilityEnabled) { + resolve(); + return; + } + + let observe = (subject, topic, data) => { + if (data === "1") { + Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); + resolve(); + } + }; + Services.obs.addObserver(observe, "a11y-init-or-shutdown"); + }); + + return accService; +} + +function shutdownAccessibilityService() { + forceGC(); + + return new Promise(resolve => { + if (!Services.appinfo.accessibilityEnabled) { + resolve(); + return; + } + + let observe = (subject, topic, data) => { + if (data === "0") { + Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); + resolve(); + } + }; + Services.obs.addObserver(observe, "a11y-init-or-shutdown"); + }); +} diff --git a/accessible/tests/browser/head.js b/accessible/tests/browser/head.js new file mode 100644 index 0000000000..915d502905 --- /dev/null +++ b/accessible/tests/browser/head.js @@ -0,0 +1,147 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* exported initAccService, shutdownAccService, waitForEvent, setE10sPrefs, + unsetE10sPrefs, accConsumersChanged */ + +// Load the shared-head file first. +/* import-globals-from shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +const { CommonUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm" +); + +/** + * Set e10s related preferences in the test environment. + * @return {Promise} promise that resolves when preferences are set. + */ +function setE10sPrefs() { + return new Promise(resolve => + SpecialPowers.pushPrefEnv( + { + set: [["browser.tabs.remote.autostart", true]], + }, + resolve + ) + ); +} + +/** + * Unset e10s related preferences in the test environment. + * @return {Promise} promise that resolves when preferences are unset. + */ +function unsetE10sPrefs() { + return new Promise(resolve => { + SpecialPowers.popPrefEnv(resolve); + }); +} + +/** + * Capture when 'a11y-consumers-changed' event is fired. + * + * @param {?Object} target + * [optional] browser object that indicates that accessibility service + * is in content process. + * @return {Array} + * List of promises where first one is the promise for when the event + * observer is added and the second one for when the event is observed. + */ +function accConsumersChanged(target) { + return target + ? [ + SpecialPowers.spawn(target, [], () => + content.CommonUtils.addAccConsumersChangedObserver() + ), + SpecialPowers.spawn(target, [], () => + content.CommonUtils.observeAccConsumersChanged() + ), + ] + : [ + CommonUtils.addAccConsumersChangedObserver(), + CommonUtils.observeAccConsumersChanged(), + ]; +} + +/** + * Capture when accessibility service is initialized. + * + * @param {?Object} target + * [optional] browser object that indicates that accessibility service + * is expected to be initialized in content process. + * @return {Array} + * List of promises where first one is the promise for when the event + * observer is added and the second one for when the event is observed. + */ +function initAccService(target) { + return target + ? [ + SpecialPowers.spawn(target, [], () => + content.CommonUtils.addAccServiceInitializedObserver() + ), + SpecialPowers.spawn(target, [], () => + content.CommonUtils.observeAccServiceInitialized() + ), + ] + : [ + CommonUtils.addAccServiceInitializedObserver(), + CommonUtils.observeAccServiceInitialized(), + ]; +} + +/** + * Capture when accessibility service is shutdown. + * + * @param {?Object} target + * [optional] browser object that indicates that accessibility service + * is expected to be shutdown in content process. + * @return {Array} + * List of promises where first one is the promise for when the event + * observer is added and the second one for when the event is observed. + */ +function shutdownAccService(target) { + return target + ? [ + SpecialPowers.spawn(target, [], () => + content.CommonUtils.addAccServiceShutdownObserver() + ), + SpecialPowers.spawn(target, [], () => + content.CommonUtils.observeAccServiceShutdown() + ), + ] + : [ + CommonUtils.addAccServiceShutdownObserver(), + CommonUtils.observeAccServiceShutdown(), + ]; +} + +/** + * Simpler verions of waitForEvent defined in + * accessible/tests/browser/events.js + */ +function waitForEvent(eventType, expectedId) { + return new Promise(resolve => { + let eventObserver = { + observe(subject) { + let event = subject.QueryInterface(Ci.nsIAccessibleEvent); + let id; + try { + id = event.accessible.id; + } catch (e) { + // This can throw NS_ERROR_FAILURE. + } + if (event.eventType === eventType && id === expectedId) { + Services.obs.removeObserver(this, "accessible-event"); + resolve(event); + } + }, + }; + Services.obs.addObserver(eventObserver, "accessible-event"); + }); +} diff --git a/accessible/tests/browser/hittest/browser.ini b/accessible/tests/browser/hittest/browser.ini new file mode 100644 index 0000000000..6b293b24bf --- /dev/null +++ b/accessible/tests/browser/hittest/browser.ini @@ -0,0 +1,15 @@ +[DEFAULT] +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + +[browser_test_browser.js] +[browser_test_canvas_hitregion.js] +skip-if = os == "android" +[browser_test_general.js] +[browser_test_shadowroot.js] +[browser_test_zoom_text.js] +[browser_test_zoom.js] diff --git a/accessible/tests/browser/hittest/browser_test_browser.js b/accessible/tests/browser/hittest/browser_test_browser.js new file mode 100644 index 0000000000..477af42fe9 --- /dev/null +++ b/accessible/tests/browser/hittest/browser_test_browser.js @@ -0,0 +1,68 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +async function runTests(browser, accDoc) { + // Hit testing. See bug #726097 + await invokeContentTask(browser, [], () => + content.document.getElementById("hittest").scrollIntoView(true) + ); + + const dpr = await getContentDPR(browser); + const hititem = findAccessibleChildByID(accDoc, "hititem"); + const hittest = findAccessibleChildByID(accDoc, "hittest"); + const outerDocAcc = accDoc.parent; + const rootAcc = CommonUtils.getRootAccessible(document); + + const [hitX, hitY, hitWidth, hitHeight] = Layout.getBounds(hititem, dpr); + // "hititem" node has the full screen width, so when we divide it by 2, we are + // still way outside the inline content. + const tgtX = hitX + hitWidth / 2; + const tgtY = hitY + hitHeight / 2; + + let hitAcc = rootAcc.getDeepestChildAtPoint(tgtX, tgtY); + is( + hitAcc, + hititem, + `Hit match at ${tgtX},${tgtY} (root doc deepest child). Found: ${prettyName( + hitAcc + )}` + ); + + const hitAcc2 = accDoc.getDeepestChildAtPoint(tgtX, tgtY); + is( + hitAcc, + hitAcc2, + `Hit match at ${tgtX},${tgtY} (doc deepest child). Found: ${prettyName( + hitAcc2 + )}` + ); + + hitAcc = outerDocAcc.getChildAtPoint(tgtX, tgtY); + is( + hitAcc, + accDoc, + `Hit match at ${tgtX},${tgtY} (outer doc child). Found: ${prettyName( + hitAcc + )}` + ); + + hitAcc = accDoc.getChildAtPoint(tgtX, tgtY); + is( + hitAcc, + hittest, + `Hit match at ${tgtX},${tgtY} (doc child). Found: ${prettyName(hitAcc)}` + ); +} + +addAccessibleTask( + ` + <div id="hittest"> + <div id="hititem"><span role="img">img</span>item</div> + </div> + `, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/hittest/browser_test_canvas_hitregion.js b/accessible/tests/browser/hittest/browser_test_canvas_hitregion.js new file mode 100644 index 0000000000..dbd326f593 --- /dev/null +++ b/accessible/tests/browser/hittest/browser_test_canvas_hitregion.js @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function convertCSSToDevicePixels(browser, ...bounds) { + return invokeContentTask(browser, bounds, (x, y, width, height) => { + const { Layout: LayoutUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + + return LayoutUtils.CSSToDevicePixels(content, x, y, width, height); + }); +} + +async function runTests(browser, accDoc) { + Services.prefs.setBoolPref("canvas.hitregions.enabled", true); + + const offsetX = 20; + const offsetY = 40; + await invokeContentTask(browser, [], () => + content.document.getElementById("hitcanvas").scrollIntoView(true) + ); + const dpr = await getContentDPR(browser); + + await invokeContentTask(browser, [offsetX, offsetY], (x, y) => { + const doc = content.document; + const element = doc.getElementById("hitcheck"); + const context = doc.getElementById("hitcanvas").getContext("2d"); + context.save(); + context.font = "10px sans-serif"; + context.textAlign = "left"; + context.textBaseline = "middle"; + const metrics = context.measureText(element.parentNode.textContent); + context.beginPath(); + context.strokeStyle = "black"; + context.rect(x - 5, y - 5, 10, 10); + context.stroke(); + if (element.checked) { + context.fillStyle = "black"; + context.fill(); + } + + context.fillText(element.parentNode.textContent, x + 5, y); + context.beginPath(); + context.rect(x - 7, y - 7, 12 + metrics.width + 2, 14); + + if (doc.activeElement == element) { + context.drawFocusIfNeeded(element); + } + + context.addHitRegion({ control: element }); + context.restore(); + }); + + const hitcanvas = findAccessibleChildByID(accDoc, "hitcanvas"); + const hitcheck = findAccessibleChildByID(accDoc, "hitcheck"); + const [hitX, hitY /* hitWidth, hitHeight */] = Layout.getBounds( + hitcanvas, + dpr + ); + const [deltaX, deltaY] = await convertCSSToDevicePixels( + browser, + offsetX, + offsetY + ); + + info("Test if we hit the region associated with the shadow dom checkbox."); + const tgtX = hitX + deltaX; + let tgtY = hitY + deltaY; + let hitAcc = accDoc.getDeepestChildAtPoint(tgtX, tgtY); + CommonUtils.isObject(hitAcc, hitcheck, `Hit match at (${tgtX}, ${tgtY}`); + + info( + "Test that we don't hit the region associated with the shadow dom checkbox." + ); + tgtY = hitY + deltaY * 2; + hitAcc = accDoc.getDeepestChildAtPoint(tgtX, tgtY); + CommonUtils.isObject(hitAcc, hitcanvas, `Hit match at (${tgtX}, ${tgtY}`); + + Services.prefs.clearUserPref("canvas.hitregions.enabled"); +} + +addAccessibleTask( + ` + <canvas id="hitcanvas"> + <input id="hitcheck" type="checkbox"><label for="showA"> Show A </label> + </canvas>`, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/hittest/browser_test_general.js b/accessible/tests/browser/hittest/browser_test_general.js new file mode 100644 index 0000000000..08596529ee --- /dev/null +++ b/accessible/tests/browser/hittest/browser_test_general.js @@ -0,0 +1,123 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +async function runTests(browser, accDoc) { + await waitForImageMap(browser, accDoc); + const dpr = await getContentDPR(browser); + + info("Not specific case, child and deepchild testing."); + testChildAtPoint( + dpr, + 3, + 3, + findAccessibleChildByID(accDoc, "list"), + findAccessibleChildByID(accDoc, "listitem"), + findAccessibleChildByID(accDoc, "inner").firstChild + ); + todo( + false, + "Bug 746974 - children must match on all platforms. On Windows, " + + "ChildAtPoint with eDeepestChild is incorrectly ignoring MustPrune " + + "for the graphic." + ); + + info( + "::MustPrune case (in this case childAtPoint doesn't look inside a " + + "textbox), point is inside of textbox." + ); + const txt = findAccessibleChildByID(accDoc, "txt"); + testChildAtPoint(dpr, 1, 1, txt, txt, txt); + + info( + "::MustPrune case, point is outside of textbox accessible but is in document." + ); + testChildAtPoint(dpr, -1, -1, txt, null, null); + + info("::MustPrune case, point is outside of root accessible."); + testChildAtPoint(dpr, -10000, -10000, txt, null, null); + + info("Not specific case, point is inside of btn accessible."); + const btn = findAccessibleChildByID(accDoc, "btn"); + testChildAtPoint(dpr, 1, 1, btn, btn, btn); + + info("Not specific case, point is outside of btn accessible."); + testChildAtPoint(dpr, -1, -1, btn, null, null); + + info( + "Out of flow accessible testing, do not return out of flow accessible " + + "because it's not a child of the accessible even though visually it is." + ); + await invokeContentTask(browser, [], () => { + const { CommonUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm" + ); + + const doc = content.document; + const rectArea = CommonUtils.getNode("area", doc).getBoundingClientRect(); + const outOfFlow = CommonUtils.getNode("outofflow", doc); + outOfFlow.style.left = rectArea.left + "px"; + outOfFlow.style.top = rectArea.top + "px"; + }); + + const area = findAccessibleChildByID(accDoc, "area"); + testChildAtPoint(dpr, 1, 1, area, area, area); + + info("Test image maps. Their children are not in the layout tree."); + const imgmap = findAccessibleChildByID(accDoc, "imgmap"); + const theLetterA = imgmap.firstChild; + await hitTest(browser, imgmap, theLetterA, theLetterA); + await hitTest( + browser, + findAccessibleChildByID(accDoc, "container"), + imgmap, + theLetterA + ); + + info("hit testing for element contained by zero-width element"); + const container2Input = findAccessibleChildByID(accDoc, "container2_input"); + await hitTest( + browser, + findAccessibleChildByID(accDoc, "container2"), + container2Input, + container2Input + ); +} + +addAccessibleTask( + ` + <div role="list" id="list"> + <div role="listitem" id="listitem"><span title="foo" id="inner">inner</span>item</div> + </div> + + <span role="button">button1</span><span role="button" id="btn">button2</span> + + <span role="textbox">textbox1</span><span role="textbox" id="txt">textbox2</span> + + <div id="outofflow" style="width: 10px; height: 10px; position: absolute; left: 0px; top: 0px; background-color: yellow;"> + </div> + <div id="area" style="width: 100px; height: 100px; background-color: blue;"></div> + + <map name="atoz_map"> + <area id="thelettera" href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a" + coords="0,0,15,15" alt="thelettera" shape="rect"/> + </map> + + <div id="container"> + <img id="imgmap" width="447" height="15" usemap="#atoz_map" src="http://example.com/a11y/accessible/tests/mochitest/letters.gif"/> + </div> + + <div id="container2" style="width: 0px"> + <input id="container2_input"> + </div> + `, + runTests, + { + iframe: true, + remoteIframe: true, + // Ensure that all hittest elements are in view. + iframeAttrs: { style: "width: 600px; height: 600px;" }, + } +); diff --git a/accessible/tests/browser/hittest/browser_test_shadowroot.js b/accessible/tests/browser/hittest/browser_test_shadowroot.js new file mode 100644 index 0000000000..cb499a4fd9 --- /dev/null +++ b/accessible/tests/browser/hittest/browser_test_shadowroot.js @@ -0,0 +1,61 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +async function runTests(browser, accDoc) { + const dpr = await getContentDPR(browser); + let componentAcc = findAccessibleChildByID(accDoc, "component1"); + testChildAtPoint( + dpr, + 1, + 1, + componentAcc, + componentAcc.firstChild, + componentAcc.firstChild + ); + + componentAcc = findAccessibleChildByID(accDoc, "component2"); + testChildAtPoint( + dpr, + 1, + 1, + componentAcc, + componentAcc.firstChild, + componentAcc.firstChild + ); +} + +addAccessibleTask( + ` + <div role="group" class="components" id="component1" style="display: inline-block;"> + <!-- + <div role="button" id="component-child" + style="width: 100px; height: 100px; background-color: pink;"> + </div> + --> + </div> + <div role="group" class="components" id="component2" style="display: inline-block;"> + <!-- + <button>Hello world</button> + --> + </div> + <script> + // This routine adds the comment children of each 'component' to its + // shadow root. + var components = document.querySelectorAll(".components"); + for (var i = 0; i < components.length; i++) { + var component = components[i]; + var shadow = component.attachShadow({mode: "open"}); + for (var child = component.firstChild; child; child = child.nextSibling) { + if (child.nodeType === 8) + // eslint-disable-next-line no-unsanitized/property + shadow.innerHTML = child.data; + } + } + </script> + `, + runTests, + { iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/hittest/browser_test_zoom.js b/accessible/tests/browser/hittest/browser_test_zoom.js new file mode 100644 index 0000000000..2ea330fb74 --- /dev/null +++ b/accessible/tests/browser/hittest/browser_test_zoom.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +async function runTests(browser, accDoc) { + if (Services.appinfo.OS !== "Darwin") { + const p1 = findAccessibleChildByID(accDoc, "p1"); + const p2 = findAccessibleChildByID(accDoc, "p2"); + await hitTest(browser, accDoc, p1, p1.firstChild); + await hitTest(browser, accDoc, p2, p2.firstChild); + + await invokeContentTask(browser, [], () => { + const { Layout } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + + Layout.zoomDocument(content.document, 2.0); + content.document.body.offsetTop; // getBounds doesn't flush layout on its own. + }); + + await hitTest(browser, accDoc, p1, p1.firstChild); + await hitTest(browser, accDoc, p2, p2.firstChild); + } else { + todo( + false, + "Bug 746974 - deepest child must be correct on all platforms, disabling on Mac!" + ); + } +} + +addAccessibleTask(`<p id="p1">para 1</p><p id="p2">para 2</p>`, runTests, { + iframe: true, + remoteIframe: true, + // Ensure that all hittest elements are in view. + iframeAttrs: { style: "left: 100px; top: 100px;" }, +}); diff --git a/accessible/tests/browser/hittest/browser_test_zoom_text.js b/accessible/tests/browser/hittest/browser_test_zoom_text.js new file mode 100644 index 0000000000..0d7eb336ca --- /dev/null +++ b/accessible/tests/browser/hittest/browser_test_zoom_text.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test if getOffsetAtPoint returns the given text offset at given coordinates. + */ +function testOffsetAtPoint(hyperText, x, y, coordType, expectedOffset) { + is( + hyperText.getOffsetAtPoint(x, y, coordType), + expectedOffset, + `Wrong offset at given point (${x}, ${y}) for ${prettyName(hyperText)}` + ); +} + +async function runTests(browser, accDoc) { + const expectedLength = await invokeContentTask(browser, [], () => { + const { CommonUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm" + ); + const hyperText = CommonUtils.getNode("paragraph", content.document); + + return hyperText.textContent.length / 2; + }); + const hyperText = findAccessibleChildByID(accDoc, "paragraph", [ + Ci.nsIAccessibleText, + ]); + const textNode = hyperText.firstChild; + + let [x, y, width, height] = Layout.getBounds( + textNode, + await getContentDPR(browser) + ); + + testOffsetAtPoint( + hyperText, + x + width / 2, + y + height / 2, + COORDTYPE_SCREEN_RELATIVE, + expectedLength + ); + + await invokeContentTask(browser, [], () => { + const { Layout } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + + Layout.zoomDocument(content.document, 2.0); + content.document.body.offsetTop; // getBounds doesn't flush layout on its own. + }); + + [x, y, width, height] = Layout.getBounds( + textNode, + await getContentDPR(browser) + ); + + testOffsetAtPoint( + hyperText, + x + width / 2, + y + height / 2, + COORDTYPE_SCREEN_RELATIVE, + expectedLength + ); +} + +addAccessibleTask( + `<p id="paragraph" style="font-family: monospace;">Болтали две сороки</p>`, + runTests, + { + iframe: true, + remoteIframe: true, + iframeAttrs: { style: "width: 600px; height: 600px;" }, + } +); diff --git a/accessible/tests/browser/hittest/head.js b/accessible/tests/browser/hittest/head.js new file mode 100644 index 0000000000..f06467da7c --- /dev/null +++ b/accessible/tests/browser/hittest/head.js @@ -0,0 +1,89 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ + +/* exported CommonUtils, testChildAtPoint, Layout, hitTest */ + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); + +const { CommonUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Common.jsm" +); + +const { Layout } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" +); + +function getChildAtPoint(container, x, y, findDeepestChild) { + try { + return findDeepestChild + ? container.getDeepestChildAtPoint(x, y) + : container.getChildAtPoint(x, y); + } catch (e) { + // Failed to get child at point. + } + + return null; +} + +function testChildAtPoint(dpr, x, y, container, child, grandChild) { + const [containerX, containerY] = Layout.getBounds(container, dpr); + x += containerX; + y += containerY; + + CommonUtils.isObject( + getChildAtPoint(container, x, y, false), + child, + `Wrong direct child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName( + container + )}` + ); + + CommonUtils.isObject( + getChildAtPoint(container, x, y, true), + grandChild, + `Wrong deepest child accessible at the point (${x}, ${y}) of ${CommonUtils.prettyName( + container + )}` + ); +} + +/** + * Test if getChildAtPoint returns the given child and grand child accessibles + * at coordinates of child accessible (direct and deep hit test). + */ +async function hitTest(browser, container, child, grandChild) { + const [childX, childY] = await getContentBoundsForDOMElm( + browser, + getAccessibleDOMNodeID(child) + ); + const x = childX + 1; + const y = childY + 1; + + CommonUtils.isObject( + getChildAtPoint(container, x, y, false), + child, + `Wrong direct child of ${prettyName(container)}` + ); + + CommonUtils.isObject( + getChildAtPoint(container, x, y, true), + grandChild, + `Wrong deepest child of ${prettyName(container)}` + ); +} diff --git a/accessible/tests/browser/mac/browser.ini b/accessible/tests/browser/mac/browser.ini new file mode 100644 index 0000000000..ddcfe0e081 --- /dev/null +++ b/accessible/tests/browser/mac/browser.ini @@ -0,0 +1,50 @@ +[DEFAULT] +skip-if = os != 'mac' +support-files = + head.js + doc_aria_tabs.html + doc_textmarker_test.html + doc_rich_listbox.xhtml + doc_menulist.xhtml + doc_tree.xhtml + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + !/accessible/tests/mochitest/moz.png + +[browser_app.js] +[browser_aria_current.js] +[browser_details_summary.js] +[browser_label_title.js] +[browser_range.js] +[browser_roles_elements.js] +[browser_table.js] +[browser_selectables.js] +[browser_radio_position.js] +[browser_toggle_radio_check.js] +[browser_link.js] +[browser_aria_haspopup.js] +[browser_required.js] +[browser_popupbutton.js] +[browser_mathml.js] +[browser_input.js] +[browser_focus.js] +[browser_text_leaf.js] +[browser_webarea.js] +skip-if = os == 'mac' && !debug #1648813 +[browser_text_basics.js] +[browser_text_input.js] +skip-if = os == 'mac' && debug # Bug 1664577 +[browser_rotor.js] +[browser_rootgroup.js] +[browser_text_selection.js] +[browser_navigate.js] +[browser_outline.js] +[browser_outline_xul.js] +[browser_hierarchy.js] +[browser_menulist.js] +[browser_rich_listbox.js] +[browser_live_regions.js] +[browser_aria_busy.js] +[browser_aria_controls_flowto.js]
\ No newline at end of file diff --git a/accessible/tests/browser/mac/browser_app.js b/accessible/tests/browser/mac/browser_app.js new file mode 100644 index 0000000000..e57555b556 --- /dev/null +++ b/accessible/tests/browser/mac/browser_app.js @@ -0,0 +1,292 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function getMacAccessible(accOrElmOrID) { + return new Promise(resolve => { + let intervalId = setInterval(() => { + let acc = getAccessible(accOrElmOrID); + if (acc) { + clearInterval(intervalId); + resolve( + acc.nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface) + ); + } + }, 10); + }); +} + +/** + * Test browser tabs + */ +add_task(async () => { + let newTabs = await Promise.all([ + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,<title>Two</title>" + ), + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,<title>Three</title>" + ), + BrowserTestUtils.openNewForegroundTab( + gBrowser, + "data:text/html,<title>Four</title>" + ), + ]); + + // Mochitests spawn with a tab, and we've opened 3 more for a total of 4 tabs + is(gBrowser.tabs.length, 4, "We now have 4 open tabs"); + + let tablist = await getMacAccessible("tabbrowser-tabs"); + is( + tablist.getAttributeValue("AXRole"), + "AXTabGroup", + "Correct role for tablist" + ); + + let tabMacAccs = tablist.getAttributeValue("AXTabs"); + is(tabMacAccs.length, 4, "4 items in AXTabs"); + + let selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + + let tab = selectedTabs[0]; + is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab"); + is( + tab.getAttributeValue("AXSubrole"), + "AXTabButton", + "Correct subrole for tab" + ); + is(tab.getAttributeValue("AXTitle"), "Four", "Correct title for tab"); + + let tabToSelect = tabMacAccs[2]; + is( + tabToSelect.getAttributeValue("AXTitle"), + "Three", + "Correct title for tab" + ); + + let actions = tabToSelect.actionNames; + ok(true, actions); + ok(actions.includes("AXPress"), "Has switch action"); + + // When tab is clicked selection of tab group changes, + // and focus goes to the web area. Wait for both. + let evt = Promise.all([ + waitForMacEvent("AXSelectedChildrenChanged"), + waitForMacEvent( + "AXFocusedUIElementChanged", + iface => iface.getAttributeValue("AXRole") == "AXWebArea" + ), + ]); + tabToSelect.performAction("AXPress"); + await evt; + + selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + is( + selectedTabs[0].getAttributeValue("AXTitle"), + "Three", + "Correct title for tab" + ); + + // Close all open tabs + await Promise.all(newTabs.map(t => BrowserTestUtils.removeTab(t))); +}); + +/** + * Test ignored invisible items in root + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "about:license", + }, + async browser => { + let root = await getMacAccessible(document); + let rootChildCount = () => root.getAttributeValue("AXChildren").length; + + // With no popups, the root accessible has 5 visible children: + // 1. Tab bar (#TabsToolbar) + // 2. Navigation bar (#nav-bar) + // 3. Content area (#tabbrowser-tabpanels) + // 4. Some fullscreen pointer grabber (#fullscreen-and-pointerlock-wrapper) + // 5. Accessibility announcements dialog (#a11y-announcement) + is(rootChildCount(), 5, "Root with no popups has 5 children"); + + // Open a context menu + const menu = document.getElementById("contentAreaContextMenu"); + EventUtils.synthesizeMouseAtCenter(document.body, { + type: "contextmenu", + }); + await waitForMacEvent("AXMenuOpened"); + + // Now root has 6 children + is(rootChildCount(), 6, "Root has 6 children"); + + // Close context menu + let closed = waitForMacEvent("AXMenuClosed", "contentAreaContextMenu"); + EventUtils.synthesizeKey("KEY_Escape"); + await BrowserTestUtils.waitForPopupEvent(menu, "hidden"); + await closed; + + // We're back to 5 + is(rootChildCount(), 5, "Root has 5 children"); + + // Open site identity popup + document.getElementById("identity-box").click(); + const identityPopup = document.getElementById("identity-popup"); + await BrowserTestUtils.waitForPopupEvent(identityPopup, "shown"); + + // Now root has 6 children + is(rootChildCount(), 6, "Root has 6 children"); + + // Close popup + EventUtils.synthesizeKey("KEY_Escape"); + await BrowserTestUtils.waitForPopupEvent(identityPopup, "hidden"); + + // We're back to 5 + is(rootChildCount(), 5, "Root has 5 children"); + } + ); +}); + +/** + * Tests for location bar + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: "http://example.com", + }, + async browser => { + let input = await getMacAccessible("urlbar-input"); + is( + input.getAttributeValue("AXValue"), + "example.com", + "Location bar has correct value" + ); + } + ); +}); + +/** + * Test context menu + */ +add_task(async () => { + await BrowserTestUtils.withNewTab( + { + gBrowser, + url: + 'data:text/html,<a id="exampleLink" href="https://example.com">link</a>', + }, + async browser => { + if (!Services.search.isInitialized) { + let aStatus = await Services.search.init(); + Assert.ok(Components.isSuccessCode(aStatus)); + Assert.ok(Services.search.isInitialized); + } + + const hasContainers = + Services.prefs.getBoolPref("privacy.userContext.enabled") && + ContextualIdentityService.getPublicIdentities().length; + + // synthesize a right click on the link to open the link context menu + let menu = document.getElementById("contentAreaContextMenu"); + await BrowserTestUtils.synthesizeMouseAtCenter( + "#exampleLink", + { type: "contextmenu" }, + browser + ); + await waitForMacEvent("AXMenuOpened"); + + menu = await getMacAccessible(menu); + let menuChildren = menu.getAttributeValue("AXChildren"); + // menu contains 14 items when containers disabled, 15 items otherwise + const expectedChildCount = hasContainers ? 15 : 14; + is( + menuChildren.length, + expectedChildCount, + "Context menu on link contains 14 or 15 items depending on release" + ); + // items at indicies 4, 10, and 12 are the splitters when containers exist + // everything else should be a menu item, otherwise indicies of splitters are + // 3, 9, and 11 + const splitterIndicies = hasContainers ? [4, 10, 12] : [3, 9, 11]; + for (let i = 0; i < menuChildren.length; i++) { + if (splitterIndicies.includes(i)) { + is( + menuChildren[i].getAttributeValue("AXRole"), + "AXSplitter", + "found splitter in menu" + ); + } else { + is( + menuChildren[i].getAttributeValue("AXRole"), + "AXMenuItem", + "found menu item in menu" + ); + } + } + + // check the containers sub menu in depth if it exists + if (hasContainers) { + is( + menuChildren[1].getAttributeValue("AXVisibleChildren"), + null, + "Submenu 1 has no visible chldren when hidden" + ); + + // focus the first submenu + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + await waitForMacEvent("AXMenuOpened"); + + // after the submenu is opened, refetch it + menu = document.getElementById("contentAreaContextMenu"); + menu = await getMacAccessible(menu); + menuChildren = menu.getAttributeValue("AXChildren"); + + // verify submenu-menuitem's attributes + is( + menuChildren[1].getAttributeValue("AXChildren").length, + 1, + "Submenu 1 has one child when open" + ); + const subMenu = menuChildren[1].getAttributeValue("AXChildren")[0]; + is( + subMenu.getAttributeValue("AXRole"), + "AXMenu", + "submenu has role of menu" + ); + const subMenuChildren = subMenu.getAttributeValue("AXChildren"); + is(subMenuChildren.length, 4, "sub menu has 4 children"); + is( + subMenu.getAttributeValue("AXVisibleChildren").length, + 4, + "submenu has 4 visible children" + ); + + // close context menu + EventUtils.synthesizeKey("KEY_Escape"); + await waitForMacEvent("AXMenuClosed"); + } + + EventUtils.synthesizeKey("KEY_Escape"); + await waitForMacEvent("AXMenuClosed"); + } + ); +}); diff --git a/accessible/tests/browser/mac/browser_aria_busy.js b/accessible/tests/browser/mac/browser_aria_busy.js new file mode 100644 index 0000000000..e75d334e29 --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_busy.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test aria-busy + */ +addAccessibleTask( + `<div id="section" role="group">Hello</div>`, + async (browser, accDoc) => { + let section = getNativeInterface(accDoc, "section"); + + ok(!section.getAttributeValue("AXElementBusy"), "section is not busy"); + + let busyChanged = waitForMacEvent("AXElementBusyChanged", "section"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("section") + .setAttribute("aria-busy", "true"); + }); + await busyChanged; + + ok(section.getAttributeValue("AXElementBusy"), "section is busy"); + + busyChanged = waitForMacEvent("AXElementBusyChanged", "section"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("section") + .setAttribute("aria-busy", "false"); + }); + await busyChanged; + + ok(!section.getAttributeValue("AXElementBusy"), "section is not busy"); + } +); diff --git a/accessible/tests/browser/mac/browser_aria_controls_flowto.js b/accessible/tests/browser/mac/browser_aria_controls_flowto.js new file mode 100644 index 0000000000..c1b75b8318 --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_controls_flowto.js @@ -0,0 +1,84 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test aria-controls + */ +addAccessibleTask( + `<button aria-controls="info" id="info-button">Show info</button> + <div id="info">Information.</div> + <div id="more-info">More information.</div>`, + async (browser, accDoc) => { + const isARIAControls = (id, expectedIds) => + Assert.deepEqual( + getNativeInterface(accDoc, id) + .getAttributeValue("AXARIAControls") + .map(e => e.getAttributeValue("AXDOMIdentifier")), + expectedIds, + `"${id}" has correct AXARIAControls` + ); + + isARIAControls("info-button", ["info"]); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("info-button") + .setAttribute("aria-controls", "info more-info"); + }); + + isARIAControls("info-button", ["info", "more-info"]); + } +); + +/** + * Test aria-flowto + */ +addAccessibleTask( + `<button aria-flowto="info" id="info-button">Show info</button> + <div id="info">Information.</div> + <div id="more-info">More information.</div>`, + async (browser, accDoc) => { + const isLinkedUIElements = (id, expectedIds) => + Assert.deepEqual( + getNativeInterface(accDoc, id) + .getAttributeValue("AXLinkedUIElements") + .map(e => e.getAttributeValue("AXDOMIdentifier")), + expectedIds, + `"${id}" has correct AXARIAControls` + ); + + isLinkedUIElements("info-button", ["info"]); + + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("info-button") + .setAttribute("aria-flowto", "info more-info"); + }); + + isLinkedUIElements("info-button", ["info", "more-info"]); + } +); + +/** + * Test aria-controls + */ +addAccessibleTask( + `<input type="radio" id="cat-radio" name="animal"><label for="cat">Cat</label> + <input type="radio" id="dog-radio" name="animal" aria-flowto="info"><label for="dog">Dog</label> + <div id="info">Information.</div>`, + async (browser, accDoc) => { + const isLinkedUIElements = (id, expectedIds) => + Assert.deepEqual( + getNativeInterface(accDoc, id) + .getAttributeValue("AXLinkedUIElements") + .map(e => e.getAttributeValue("AXDOMIdentifier")), + expectedIds, + `"${id}" has correct AXARIAControls` + ); + + isLinkedUIElements("dog-radio", ["cat-radio", "dog-radio", "info"]); + } +); diff --git a/accessible/tests/browser/mac/browser_aria_current.js b/accessible/tests/browser/mac/browser_aria_current.js new file mode 100644 index 0000000000..0c5ff08a0a --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_current.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test aria-current + */ +addAccessibleTask( + `<a id="one" href="%23" aria-current="page">One</a><a id="two" href="%23">Two</a>`, + async (browser, accDoc) => { + let one = getNativeInterface(accDoc, "one"); + let two = getNativeInterface(accDoc, "two"); + + is( + one.getAttributeValue("AXARIACurrent"), + "page", + "Correct aria-current for #one" + ); + is( + two.getAttributeValue("AXARIACurrent"), + null, + "Correct aria-current for #two" + ); + + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "one"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("one") + .setAttribute("aria-current", "step"); + }); + await stateChanged; + + is( + one.getAttributeValue("AXARIACurrent"), + "step", + "Correct aria-current for #one" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "one"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("one").removeAttribute("aria-current"); + }); + await stateChanged; + + is( + one.getAttributeValue("AXARIACurrent"), + null, + "Correct aria-current for #one" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_aria_haspopup.js b/accessible/tests/browser/mac/browser_aria_haspopup.js new file mode 100644 index 0000000000..36bff246db --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_haspopup.js @@ -0,0 +1,330 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test aria-haspopup + */ +addAccessibleTask( + ` + <button aria-haspopup="false" id="false">action</button> + + <button aria-haspopup="menu" id="menu">action</button> + + <button aria-haspopup="listbox" id="listbox">action</button> + + <button aria-haspopup="tree" id="tree">action</button> + + <button aria-haspopup="grid" id="grid">action</button> + + <button aria-haspopup="dialog" id="dialog">action</button> + + `, + async (browser, accDoc) => { + // FALSE + let falseID = getNativeInterface(accDoc, "false"); + is( + falseID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup val for button with false" + ); + is( + falseID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue val for button with false" + ); + let attrChanged = waitForEvent(EVENT_STATE_CHANGE, "false"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("false") + .setAttribute("aria-haspopup", "true"); + }); + await attrChanged; + + is( + falseID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for false" + ); + is( + falseID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup val for button with true" + ); + + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "false"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("false").removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + falseID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for false" + ); + is( + falseID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup val for button after remove" + ); + + // MENU + let menuID = getNativeInterface(accDoc, "menu"); + is( + menuID.getAttributeValue("AXPopupValue"), + "menu", + "Correct AXPopupValue val for button with menu" + ); + is( + menuID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup val for button with menu" + ); + + attrChanged = waitForEvent(EVENT_STATE_CHANGE, "menu"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("menu") + .setAttribute("aria-haspopup", "true"); + }); + await attrChanged; + + is( + menuID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for menu" + ); + is( + menuID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup val for button with menu" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "menu"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("menu").removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + menuID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for menu" + ); + is( + menuID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup val for button after remove" + ); + + // LISTBOX + let listboxID = getNativeInterface(accDoc, "listbox"); + is( + listboxID.getAttributeValue("AXPopupValue"), + "listbox", + "Correct AXPopupValue for button with listbox" + ); + is( + listboxID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with listbox" + ); + + attrChanged = waitForEvent(EVENT_STATE_CHANGE, "listbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("listbox") + .setAttribute("aria-haspopup", "true"); + }); + await attrChanged; + + is( + listboxID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for listbox" + ); + is( + listboxID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with listbox" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "listbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("listbox") + .removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + listboxID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for listbox" + ); + is( + listboxID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup for button with listbox" + ); + + // TREE + let treeID = getNativeInterface(accDoc, "tree"); + is( + treeID.getAttributeValue("AXPopupValue"), + "tree", + "Correct AXPopupValue for button with tree" + ); + is( + treeID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with tree" + ); + + attrChanged = waitForEvent(EVENT_STATE_CHANGE, "tree"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("tree") + .setAttribute("aria-haspopup", "true"); + }); + await attrChanged; + + is( + treeID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for tree" + ); + is( + treeID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with tree" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "tree"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("tree").removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + treeID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for tree" + ); + is( + treeID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup for button with tree after remove" + ); + + // GRID + let gridID = getNativeInterface(accDoc, "grid"); + is( + gridID.getAttributeValue("AXPopupValue"), + "grid", + "Correct AXPopupValue for button with grid" + ); + is( + gridID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with grid" + ); + + attrChanged = waitForEvent(EVENT_STATE_CHANGE, "grid"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("grid") + .setAttribute("aria-haspopup", "true"); + }); + await attrChanged; + + is( + gridID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for grid" + ); + is( + gridID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with grid" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "grid"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("grid").removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + gridID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for grid" + ); + is( + gridID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup for button with grid after remove" + ); + + // DIALOG + let dialogID = getNativeInterface(accDoc, "dialog"); + is( + dialogID.getAttributeValue("AXPopupValue"), + "dialog", + "Correct AXPopupValue for button with dialog" + ); + is( + dialogID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with dialog" + ); + + attrChanged = waitForEvent(EVENT_STATE_CHANGE, "dialog"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("dialog") + .setAttribute("aria-haspopup", "true"); + }); + await attrChanged; + + is( + dialogID.getAttributeValue("AXPopupValue"), + "true", + "Correct AXPopupValue after change for dialog" + ); + is( + dialogID.getAttributeValue("AXHasPopup"), + 1, + "Correct AXHasPopup for button with dialog" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "dialog"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("dialog") + .removeAttribute("aria-haspopup"); + }); + await stateChanged; + + is( + dialogID.getAttributeValue("AXPopupValue"), + null, + "Correct AXPopupValue after remove for dialog" + ); + is( + dialogID.getAttributeValue("AXHasPopup"), + 0, + "Correct AXHasPopup for button with dialog after remove" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_details_summary.js b/accessible/tests/browser/mac/browser_details_summary.js new file mode 100644 index 0000000000..62183afef5 --- /dev/null +++ b/accessible/tests/browser/mac/browser_details_summary.js @@ -0,0 +1,60 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test details/summary + */ +addAccessibleTask( + `<details id="details"><summary>Foo</summary><p>Bar</p></details>`, + async (browser, accDoc) => { + let details = getNativeInterface(accDoc, "details"); + is( + details.getAttributeValue("AXRole"), + "AXGroup", + "Correct role for details" + ); + is( + details.getAttributeValue("AXSubrole"), + "AXDetails", + "Correct subrole for details" + ); + + let detailsChildren = details.getAttributeValue("AXChildren"); + is(detailsChildren.length, 1, "collapsed details has only one child"); + + let summary = detailsChildren[0]; + is( + summary.getAttributeValue("AXRole"), + "AXButton", + "Correct role for summary" + ); + is( + summary.getAttributeValue("AXSubrole"), + "AXSummary", + "Correct subrole for summary" + ); + is(summary.getAttributeValue("AXExpanded"), 0, "Summary is collapsed"); + + let actions = summary.actionNames; + ok(actions.includes("AXPress"), "Summary Has press action"); + + let reorder = waitForEvent(EVENT_REORDER, "details"); + summary.performAction("AXPress"); + is(summary.getAttributeValue("AXExpanded"), 1, "Summary is collapsed"); + // The reorder gecko event notifies us of a tree change. + await reorder; + + detailsChildren = details.getAttributeValue("AXChildren"); + is(detailsChildren.length, 2, "collapsed details has only one child"); + } +); diff --git a/accessible/tests/browser/mac/browser_focus.js b/accessible/tests/browser/mac/browser_focus.js new file mode 100644 index 0000000000..6bceb06c6c --- /dev/null +++ b/accessible/tests/browser/mac/browser_focus.js @@ -0,0 +1,44 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test focusability + */ +addAccessibleTask( + ` + <div role="button" id="ariabutton">hello</div> <button id="button">world</button> + `, + async (browser, accDoc) => { + let ariabutton = getNativeInterface(accDoc, "ariabutton"); + let button = getNativeInterface(accDoc, "button"); + + is( + ariabutton.getAttributeValue("AXFocused"), + 0, + "aria button is not focused" + ); + + is(button.getAttributeValue("AXFocused"), 0, "button is not focused"); + + ok( + !ariabutton.isAttributeSettable("AXFocused"), + "aria button should not be focusable" + ); + + ok(button.isAttributeSettable("AXFocused"), "button is focusable"); + + let evt = waitForMacEvent( + "AXFocusedUIElementChanged", + iface => iface.getAttributeValue("AXDOMIdentifier") == "button" + ); + + button.setAttributeValue("AXFocused", true); + + await evt; + + is(button.getAttributeValue("AXFocused"), 1, "button is focused"); + } +); diff --git a/accessible/tests/browser/mac/browser_hierarchy.js b/accessible/tests/browser/mac/browser_hierarchy.js new file mode 100644 index 0000000000..8a97e55c07 --- /dev/null +++ b/accessible/tests/browser/mac/browser_hierarchy.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test AXIndexForChildUIElement + */ +addAccessibleTask( + `<p id="p">Hello <a href="#" id="link">strange</a> world`, + (browser, accDoc) => { + let p = getNativeInterface(accDoc, "p"); + + let children = p.getAttributeValue("AXChildren"); + is(children.length, 3, "p has 3 children"); + is( + children[1].getAttributeValue("AXDOMIdentifier"), + "link", + "second child is link" + ); + + let index = p.getParameterizedAttributeValue( + "AXIndexForChildUIElement", + children[1] + ); + is(index, 1, "link is second child"); + } +); + +/** + * Test textbox with more than one child + */ +addAccessibleTask( + `<div id="textbox" role="textbox">Hello <a href="#">strange</a> world</div>`, + (browser, accDoc) => { + let textbox = getNativeInterface(accDoc, "textbox"); + + is( + textbox.getAttributeValue("AXChildren").length, + 3, + "textbox has 3 children" + ); + } +); + +/** + * Test textbox with one child + */ +addAccessibleTask( + `<div id="textbox" role="textbox">Hello </div>`, + async (browser, accDoc) => { + let textbox = getNativeInterface(accDoc, "textbox"); + + is( + textbox.getAttributeValue("AXChildren").length, + 0, + "textbox with one child is pruned" + ); + + let reorder = waitForEvent(EVENT_REORDER, "textbox"); + await SpecialPowers.spawn(browser, [], () => { + let link = content.document.createElement("a"); + link.textContent = "World"; + content.document.getElementById("textbox").appendChild(link); + }); + await reorder; + + is( + textbox.getAttributeValue("AXChildren").length, + 2, + "textbox with two child is not pruned" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_input.js b/accessible/tests/browser/mac/browser_input.js new file mode 100644 index 0000000000..7fa20a9d4b --- /dev/null +++ b/accessible/tests/browser/mac/browser_input.js @@ -0,0 +1,225 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function selectedTextEventPromises(stateChangeType) { + return [ + waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + info.AXTextStateChangeType == stateChangeType && + elem.getAttributeValue("AXDOMIdentifier") == "body" + ); + }), + waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + info.AXTextStateChangeType == stateChangeType && + elem.getAttributeValue("AXDOMIdentifier") == "input" + ); + }), + ]; +} + +async function testInput(browser, accDoc) { + let input = getNativeInterface(accDoc, "input"); + + is(input.getAttributeValue("AXDescription"), "Name", "Correct input label"); + is(input.getAttributeValue("AXTitle"), "", "Correct input title"); + is(input.getAttributeValue("AXValue"), "Elmer Fudd", "Correct input value"); + is( + input.getAttributeValue("AXNumberOfCharacters"), + 10, + "Correct length of value" + ); + + ok(input.attributeNames.includes("AXSelectedText"), "Has AXSelectedText"); + ok( + input.attributeNames.includes("AXSelectedTextRange"), + "Has AXSelectedTextRange" + ); + + let evt = Promise.all([ + waitForMacEvent("AXFocusedUIElementChanged", "input"), + ...selectedTextEventPromises(AXTextStateChangeTypeSelectionMove), + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("input").focus(); + }); + await evt; + + evt = Promise.all( + selectedTextEventPromises(AXTextStateChangeTypeSelectionExtend) + ); + await SpecialPowers.spawn(browser, [], () => { + let elm = content.document.getElementById("input"); + if (elm.setSelectionRange) { + elm.setSelectionRange(6, 9); + } else { + let r = new content.Range(); + let textNode = elm.firstElementChild.firstChild; + r.setStart(textNode, 6); + r.setEnd(textNode, 9); + + let s = content.getSelection(); + s.removeAllRanges(); + s.addRange(r); + } + }); + await evt; + + is( + input.getAttributeValue("AXSelectedText"), + "Fud", + "Correct text is selected" + ); + + Assert.deepEqual( + input.getAttributeValue("AXSelectedTextRange"), + [6, 3], + "correct range selected" + ); + + ok( + input.isAttributeSettable("AXSelectedTextRange"), + "AXSelectedTextRange is settable" + ); + + evt = Promise.all( + selectedTextEventPromises(AXTextStateChangeTypeSelectionExtend) + ); + input.setAttributeValue("AXSelectedTextRange", NSRange(1, 7)); + await evt; + + Assert.deepEqual( + input.getAttributeValue("AXSelectedTextRange"), + [1, 7], + "correct range selected" + ); + + is( + input.getAttributeValue("AXSelectedText"), + "lmer Fu", + "Correct text is selected" + ); + + let domSelection = await SpecialPowers.spawn(browser, [], () => { + let elm = content.document.querySelector("input#input"); + if (elm) { + return elm.value.substring(elm.selectionStart, elm.selectionEnd); + } + + return content.getSelection().toString(); + }); + + is(domSelection, "lmer Fu", "correct DOM selection"); + + is( + input.getParameterizedAttributeValue("AXStringForRange", NSRange(3, 5)), + "er Fu", + "AXStringForRange works" + ); +} + +/** + * Input selection test + */ +addAccessibleTask( + `<input aria-label="Name" id="input" value="Elmer Fudd">`, + testInput +); + +/** + * contenteditable selection test + */ +addAccessibleTask( + `<div aria-label="Name" tabindex="0" role="textbox" aria-multiline="true" id="input" contenteditable> + <p>Elmer Fudd</p> + </div>`, + testInput +); + +/** + * test contenteditable with selection that extends past editable part + */ +addAccessibleTask( + `<span aria-label="Name" + tabindex="0" + role="textbox" + id="input" + contenteditable>Elmer Fudd</span> <span id="notinput">is the name</span>`, + async (browser, accDoc) => { + let evt = Promise.all([ + waitForMacEvent("AXFocusedUIElementChanged", "input"), + waitForMacEvent("AXSelectedTextChanged", "body"), + waitForMacEvent("AXSelectedTextChanged", "input"), + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("input").focus(); + }); + await evt; + + evt = waitForEvent(EVENT_TEXT_CARET_MOVED); + await SpecialPowers.spawn(browser, [], () => { + let input = content.document.getElementById("input"); + let notinput = content.document.getElementById("notinput"); + + let r = new content.Range(); + r.setStart(input.firstChild, 4); + r.setEnd(notinput.firstChild, 6); + + let s = content.getSelection(); + s.removeAllRanges(); + s.addRange(r); + }); + await evt; + + let input = getNativeInterface(accDoc, "input"); + + is( + input.getAttributeValue("AXSelectedText"), + "r Fudd", + "Correct text is selected in #input" + ); + + is( + stringForRange( + input, + input.getAttributeValue("AXSelectedTextMarkerRange") + ), + "r Fudd is the", + "Correct text is selected in document" + ); + } +); + +/** + * test nested content editables and their ancestor getters. + */ +addAccessibleTask( + `<div id="outer" role="textbox" contenteditable="true"> + <p id="p">Bob <a href="#" id="link">Loblaw's</a></p> + <div id="inner" role="textbox" contenteditable="true"> + Law <a href="#" id="inner_link">Blog</a> + </div> + </div>`, + (browser, accDoc) => { + let link = getNativeInterface(accDoc, "link"); + let innerLink = getNativeInterface(accDoc, "inner_link"); + + let idmatches = (elem, id) => { + is(elem.getAttributeValue("AXDOMIdentifier"), id, "Matches ID"); + }; + + idmatches(link.getAttributeValue("AXEditableAncestor"), "outer"); + idmatches(link.getAttributeValue("AXFocusableAncestor"), "outer"); + idmatches(link.getAttributeValue("AXHighestEditableAncestor"), "outer"); + + idmatches(innerLink.getAttributeValue("AXEditableAncestor"), "inner"); + idmatches(innerLink.getAttributeValue("AXFocusableAncestor"), "inner"); + idmatches( + innerLink.getAttributeValue("AXHighestEditableAncestor"), + "outer" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_label_title.js b/accessible/tests/browser/mac/browser_label_title.js new file mode 100644 index 0000000000..0ed45391e9 --- /dev/null +++ b/accessible/tests/browser/mac/browser_label_title.js @@ -0,0 +1,79 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test different labeling/titling schemes for text fields + */ +addAccessibleTask( + `<label for="n1">Label</label> <input id="n1"> + <label for="n2">Two</label> <label for="n2">Labels</label> <input id="n2"> + <input aria-label="ARIA Label" id="n3">`, + (browser, accDoc) => { + let n1 = getNativeInterface(accDoc, "n1"); + let n1Label = n1.getAttributeValue("AXTitleUIElement"); + // XXX: In Safari the label is an AXText with an AXValue, + // here it is an AXGroup witth an AXTitle + is(n1Label.getAttributeValue("AXTitle"), "Label"); + + let n2 = getNativeInterface(accDoc, "n2"); + is(n2.getAttributeValue("AXDescription"), "TwoLabels"); + + let n3 = getNativeInterface(accDoc, "n3"); + is(n3.getAttributeValue("AXDescription"), "ARIA Label"); + } +); + +/** + * Test to see that named groups get labels + */ +addAccessibleTask( + `<fieldset id="fieldset"><legend>Fields</legend><input aria-label="hello"></fieldset>`, + (browser, accDoc) => { + let fieldset = getNativeInterface(accDoc, "fieldset"); + is(fieldset.getAttributeValue("AXDescription"), "Fields"); + } +); + +/** + * Test to see that list items don't get titled groups + */ +addAccessibleTask( + `<ul style="list-style: none;"><li id="unstyled-item">Hello</li></ul> + <ul><li id="styled-item">World</li></ul>`, + (browser, accDoc) => { + let unstyledItem = getNativeInterface(accDoc, "unstyled-item"); + is(unstyledItem.getAttributeValue("AXTitle"), ""); + + let styledItem = getNativeInterface(accDoc, "unstyled-item"); + is(styledItem.getAttributeValue("AXTitle"), ""); + } +); + +/** + * Test that we fire a title changed notification + */ +addAccessibleTask( + `<article id="article" aria-label="Hello world"></article>`, + async (browser, accDoc) => { + let article = getNativeInterface(accDoc, "article"); + is(article.getAttributeValue("AXTitle"), "Hello world"); + let evt = waitForMacEvent("AXTitleChanged", "article"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("article") + .setAttribute("aria-label", "Hello universe"); + }); + await evt; + is(article.getAttributeValue("AXTitle"), "Hello universe"); + } +); diff --git a/accessible/tests/browser/mac/browser_link.js b/accessible/tests/browser/mac/browser_link.js new file mode 100644 index 0000000000..4ae128fbf6 --- /dev/null +++ b/accessible/tests/browser/mac/browser_link.js @@ -0,0 +1,125 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +/** + * Test visited link properties. + */ +addAccessibleTask( + ` + <a id="link" href="http://www.example.com/">I am a non-visited link</a><br> + `, + async (browser, accDoc) => { + let link = getNativeInterface(accDoc, "link"); + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link"); + + is(link.getAttributeValue("AXVisited"), 0, "Link has not been visited"); + + await PlacesTestUtils.addVisits(["http://www.example.com/"]); + + await stateChanged; + is(link.getAttributeValue("AXVisited"), 1, "Link has been visited"); + + // Ensure history is cleared before running + await PlacesUtils.history.clear(); + } +); + +/** + * Test linked vs unlinked anchor tags + */ +addAccessibleTask( + ` + <a id="link1" href="#">I am a link link</a> + <a id="link2" onclick="console.log('hi')">I am a link-ish link</a> + <a id="link3">I am a non-link link</a> + `, + async (browser, accDoc) => { + let link1 = getNativeInterface(accDoc, "link1"); + is( + link1.getAttributeValue("AXRole"), + "AXLink", + "a[href] gets correct link role" + ); + ok( + link1.attributeNames.includes("AXVisited"), + "Link has visited attribute" + ); + ok(link1.attributeNames.includes("AXURL"), "Link has URL attribute"); + + let link2 = getNativeInterface(accDoc, "link2"); + is( + link2.getAttributeValue("AXRole"), + "AXLink", + "a[onclick] gets correct link role" + ); + ok( + link2.attributeNames.includes("AXVisited"), + "Link has visited attribute" + ); + ok(link2.attributeNames.includes("AXURL"), "Link has URL attribute"); + + let link3 = getNativeInterface(accDoc, "link3"); + is( + link3.getAttributeValue("AXRole"), + "AXGroup", + "bare <a> gets correct group role" + ); + + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link1"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("link1").removeAttribute("href"); + }); + await stateChanged; + is( + link1.getAttributeValue("AXRole"), + "AXGroup", + "<a> stripped from href gets group role" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link2"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("link2").removeAttribute("onclick"); + }); + await stateChanged; + is( + link2.getAttributeValue("AXRole"), + "AXGroup", + "<a> stripped from onclick gets group role" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "link3"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("link3") + .setAttribute("href", "http://example.com"); + }); + await stateChanged; + is( + link3.getAttributeValue("AXRole"), + "AXLink", + "href added to bare a gets link role" + ); + + ok( + link3.attributeNames.includes("AXVisited"), + "Link has visited attribute" + ); + ok(link3.attributeNames.includes("AXURL"), "Link has URL attribute"); + } +); diff --git a/accessible/tests/browser/mac/browser_live_regions.js b/accessible/tests/browser/mac/browser_live_regions.js new file mode 100644 index 0000000000..10a03120f8 --- /dev/null +++ b/accessible/tests/browser/mac/browser_live_regions.js @@ -0,0 +1,165 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test live region creation and removal. + */ +addAccessibleTask( + ` + <div id="polite" aria-relevant="removals">Polite region</div> + <div id="assertive" aria-live="assertive">Assertive region</div> + `, + async (browser, accDoc) => { + let politeRegion = getNativeInterface(accDoc, "polite"); + ok( + !politeRegion.attributeNames.includes("AXARIALive"), + "region is not live" + ); + + let liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "polite"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("polite") + .setAttribute("aria-atomic", "true"); + content.document + .getElementById("polite") + .setAttribute("aria-live", "polite"); + }); + await liveRegionAdded; + is( + politeRegion.getAttributeValue("AXARIALive"), + "polite", + "region is now live" + ); + ok(politeRegion.getAttributeValue("AXARIAAtomic"), "region is atomic"); + is( + politeRegion.getAttributeValue("AXARIARelevant"), + "removals", + "region has defined aria-relevant" + ); + + let assertiveRegion = getNativeInterface(accDoc, "assertive"); + is( + assertiveRegion.getAttributeValue("AXARIALive"), + "assertive", + "region is assertive" + ); + ok( + !assertiveRegion.getAttributeValue("AXARIAAtomic"), + "region is not atomic" + ); + is( + assertiveRegion.getAttributeValue("AXARIARelevant"), + "additions text", + "region has default aria-relevant" + ); + + let liveRegionRemoved = waitForEvent( + EVENT_LIVE_REGION_REMOVED, + "assertive" + ); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("assertive").removeAttribute("aria-live"); + }); + await liveRegionRemoved; + ok(!assertiveRegion.getAttributeValue("AXARIALive"), "region is not live"); + + liveRegionAdded = waitForMacEvent("AXLiveRegionCreated", "new-region"); + await SpecialPowers.spawn(browser, [], () => { + let newRegionElm = content.document.createElement("div"); + newRegionElm.id = "new-region"; + newRegionElm.setAttribute("aria-live", "assertive"); + content.document.body.appendChild(newRegionElm); + }); + await liveRegionAdded; + + let newRegion = getNativeInterface(accDoc, "new-region"); + is( + newRegion.getAttributeValue("AXARIALive"), + "assertive", + "region is assertive" + ); + + let loadComplete = Promise.all([ + waitForMacEvent("AXLoadComplete"), + waitForMacEvent("AXLiveRegionCreated", "region-1"), + waitForMacEvent("AXLiveRegionCreated", "region-2"), + waitForMacEvent("AXLiveRegionCreated", "status"), + waitForMacEvent("AXLiveRegionCreated", "output"), + ]); + + await SpecialPowers.spawn(browser, [], () => { + content.location = `data:text/html;charset=utf-8, + <div id="region-1" aria-live="polite"></div> + <div id="region-2" aria-live="assertive"></div> + <div id="region-3" aria-live="off"></div> + <div id="alert" role="alert"></div> + <div id="status" role="status"></div> + <output id="output"></output>`; + }); + let webArea = (await loadComplete)[0]; + + is(webArea.getAttributeValue("AXRole"), "AXWebArea", "web area yeah"); + const searchPred = { + AXSearchKey: "AXLiveRegionSearchKey", + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + const liveRegions = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + Assert.deepEqual( + liveRegions.map(r => r.getAttributeValue("AXDOMIdentifier")), + ["region-1", "region-2", "alert", "status", "output"], + "SearchPredicate returned all live regions" + ); + } +); + +/** + * Test live region changes + */ +addAccessibleTask( + ` + <div id="live" aria-live="polite"> + The time is <span id="time">4:55pm</span> + <p id="p" style="display: none">Georgia on my mind</p> + <button id="button" aria-label="Start"></button> + </div> + `, + async (browser, accDoc) => { + let liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("time").textContent = "4:56pm"; + }); + await liveRegionChanged; + ok(true, "changed textContent"); + + liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("p").style.display = "block"; + }); + await liveRegionChanged; + ok(true, "changed display style to block"); + + liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("p").style.display = "none"; + }); + await liveRegionChanged; + ok(true, "changed display style to none"); + + liveRegionChanged = waitForMacEvent("AXLiveRegionChanged", "live"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("button") + .setAttribute("aria-label", "Stop"); + }); + await liveRegionChanged; + ok(true, "changed aria-label"); + } +); diff --git a/accessible/tests/browser/mac/browser_mathml.js b/accessible/tests/browser/mac/browser_mathml.js new file mode 100644 index 0000000000..1afaa8399f --- /dev/null +++ b/accessible/tests/browser/mac/browser_mathml.js @@ -0,0 +1,151 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function testMathAttr(iface, attr, subrole, textLeafValue) { + ok(iface.attributeNames.includes(attr), `Object has ${attr} attribute`); + let value = iface.getAttributeValue(attr); + is( + value.getAttributeValue("AXSubrole"), + subrole, + `${attr} value has correct subrole` + ); + + if (textLeafValue) { + let children = value.getAttributeValue("AXChildren"); + is(children.length, 1, `${attr} value has one child`); + + is( + children[0].getAttributeValue("AXRole"), + "AXStaticText", + `${attr} value's child is static text` + ); + is( + children[0].getAttributeValue("AXValue"), + textLeafValue, + `${attr} value has correct text` + ); + } +} + +addAccessibleTask( + `<math id="math"> + <msqrt id="sqrt"> + <mi>-1</mi> + </msqrt> + </math>`, + async (browser, accDoc) => { + let math = getNativeInterface(accDoc, "math"); + is( + math.getAttributeValue("AXSubrole"), + "AXDocumentMath", + "Math element has correct subrole" + ); + + let sqrt = getNativeInterface(accDoc, "sqrt"); + is( + sqrt.getAttributeValue("AXSubrole"), + "AXMathSquareRoot", + "msqrt has correct subrole" + ); + + testMathAttr(sqrt, "AXMathRootRadicand", "AXMathIdentifier", "-1"); + } +); + +addAccessibleTask( + `<math> + <mroot id="root"> + <mi>x</mi> + <mn>3</mn> + </mroot> + </math>`, + async (browser, accDoc) => { + let root = getNativeInterface(accDoc, "root"); + is( + root.getAttributeValue("AXSubrole"), + "AXMathRoot", + "mroot has correct subrole" + ); + + testMathAttr(root, "AXMathRootRadicand", "AXMathIdentifier", "x"); + testMathAttr(root, "AXMathRootIndex", "AXMathNumber", "3"); + } +); + +addAccessibleTask( + `<math> + <mfrac id="fraction"> + <mi>a</mi> + <mi>b</mi> + </mfrac> + </math>`, + async (browser, accDoc) => { + let fraction = getNativeInterface(accDoc, "fraction"); + is( + fraction.getAttributeValue("AXSubrole"), + "AXMathFraction", + "mfrac has correct subrole" + ); + ok(fraction.attributeNames.includes("AXMathFractionNumerator")); + ok(fraction.attributeNames.includes("AXMathFractionDenominator")); + ok(fraction.attributeNames.includes("AXMathLineThickness")); + + // Bug 1639745 + todo_is(fraction.getAttributeValue("AXMathLineThickness"), 1); + + testMathAttr(fraction, "AXMathFractionNumerator", "AXMathIdentifier", "a"); + testMathAttr( + fraction, + "AXMathFractionDenominator", + "AXMathIdentifier", + "b" + ); + } +); + +addAccessibleTask( + `<math> + <msubsup id="subsup"> + <mo>∫</mo> + <mn>0</mn> + <mn>1</mn> + </msubsup> + </math>`, + async (browser, accDoc) => { + let subsup = getNativeInterface(accDoc, "subsup"); + is( + subsup.getAttributeValue("AXSubrole"), + "AXMathSubscriptSuperscript", + "msubsup has correct subrole" + ); + + testMathAttr(subsup, "AXMathSubscript", "AXMathNumber", "0"); + testMathAttr(subsup, "AXMathSuperscript", "AXMathNumber", "1"); + testMathAttr(subsup, "AXMathBase", "AXMathOperator", "∫"); + } +); + +addAccessibleTask( + `<math> + <munderover id="underover"> + <mo>∫</mo> + <mn>0</mn> + <mi>∞</mi> + </munderover> + </math>`, + async (browser, accDoc) => { + let underover = getNativeInterface(accDoc, "underover"); + is( + underover.getAttributeValue("AXSubrole"), + "AXMathUnderOver", + "munderover has correct subrole" + ); + + testMathAttr(underover, "AXMathUnder", "AXMathNumber", "0"); + testMathAttr(underover, "AXMathOver", "AXMathIdentifier", "∞"); + testMathAttr(underover, "AXMathBase", "AXMathOperator", "∫"); + } +); diff --git a/accessible/tests/browser/mac/browser_menulist.js b/accessible/tests/browser/mac/browser_menulist.js new file mode 100644 index 0000000000..dd037c01ee --- /dev/null +++ b/accessible/tests/browser/mac/browser_menulist.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/attributes.js */ +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR }, + { name: "attributes.js", dir: MOCHITESTS_DIR } +); + +addAccessibleTask( + "mac/doc_menulist.xhtml", + async (browser, accDoc) => { + const menulist = getNativeInterface(accDoc, "defaultZoom"); + + let actions = menulist.actionNames; + ok(actions.includes("AXPress"), "menu has press action"); + + let event = waitForMacEvent("AXMenuOpened"); + menulist.performAction("AXPress"); + const menupopup = await event; + + const menuItems = menupopup.getAttributeValue("AXChildren"); + is(menuItems.length, 4, "Found four children in menulist"); + is( + menuItems[0].getAttributeValue("AXTitle"), + "50%", + "First item has correct title" + ); + is( + menuItems[1].getAttributeValue("AXTitle"), + "100%", + "Second item has correct title" + ); + is( + menuItems[2].getAttributeValue("AXTitle"), + "150%", + "Third item has correct title" + ); + is( + menuItems[3].getAttributeValue("AXTitle"), + "200%", + "Fourth item has correct title" + ); + }, + { topLevel: false, chrome: true } +); diff --git a/accessible/tests/browser/mac/browser_navigate.js b/accessible/tests/browser/mac/browser_navigate.js new file mode 100644 index 0000000000..69486676e4 --- /dev/null +++ b/accessible/tests/browser/mac/browser_navigate.js @@ -0,0 +1,394 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test navigation of same/different type content + */ +addAccessibleTask( + `<h1 id="hello">hello</h1> + world<br> + <a href="example.com" id="link">I am a link</a> + <h1 id="goodbye">goodbye</h1>`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXSameTypeSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: 1, + AXDirection: "AXDirectionNext", + }; + + const hello = getNativeInterface(accDoc, "hello"); + const goodbye = getNativeInterface(accDoc, "goodbye"); + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + searchPred.AXStartElement = hello; + + let sameItem = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(sameItem.length, 1, "Found one item"); + is( + "goodbye", + sameItem[0].getAttributeValue("AXTitle"), + "Found correct item of same type" + ); + + searchPred.AXDirection = "AXDirectionPrevious"; + searchPred.AXStartElement = goodbye; + sameItem = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(sameItem.length, 1, "Found one item"); + is( + "hello", + sameItem[0].getAttributeValue("AXTitle"), + "Found correct item of same type" + ); + + searchPred.AXSearchKey = "AXDifferentTypeSearchKey"; + let diffItem = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + is(diffItem.length, 1, "Found one item"); + is( + "I am a link", + diffItem[0].getAttributeValue("AXValue"), + "Found correct item of different type" + ); + } +); + +/** + * Test navigation of heading levels + */ +addAccessibleTask( + ` + <h1 id="a">a</h1> + <h2 id="b">b</h2> + <h3 id="c">c</h3> + <h4 id="d">d</h4> + <h5 id="e">e</h5> + <h6 id="f">f</h5> + <h1 id="g">g</h1> + <h2 id="h">h</h2> + <h3 id="i">i</h3> + <h4 id="j">j</h4> + <h5 id="k">k</h5> + <h6 id="l">l</h5> + this is some regular text that should be ignored + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingLevel1SearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let h1Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h1Count, "Found two h1 items"); + + let h1s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const a = getNativeInterface(accDoc, "a"); + const g = getNativeInterface(accDoc, "g"); + + is( + a.getAttributeValue("AXValue"), + h1s[0].getAttributeValue("AXValue"), + "Found correct h1 heading" + ); + + is( + g.getAttributeValue("AXValue"), + h1s[1].getAttributeValue("AXValue"), + "Found correct h1 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel2SearchKey"; + + let h2Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h2Count, "Found two h2 items"); + + let h2s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const b = getNativeInterface(accDoc, "b"); + const h = getNativeInterface(accDoc, "h"); + + is( + b.getAttributeValue("AXValue"), + h2s[0].getAttributeValue("AXValue"), + "Found correct h2 heading" + ); + + is( + h.getAttributeValue("AXValue"), + h2s[1].getAttributeValue("AXValue"), + "Found correct h2 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel3SearchKey"; + + let h3Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h3Count, "Found two h3 items"); + + let h3s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const c = getNativeInterface(accDoc, "c"); + const i = getNativeInterface(accDoc, "i"); + + is( + c.getAttributeValue("AXValue"), + h3s[0].getAttributeValue("AXValue"), + "Found correct h3 heading" + ); + + is( + i.getAttributeValue("AXValue"), + h3s[1].getAttributeValue("AXValue"), + "Found correct h3 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel4SearchKey"; + + let h4Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h4Count, "Found two h4 items"); + + let h4s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const d = getNativeInterface(accDoc, "d"); + const j = getNativeInterface(accDoc, "j"); + + is( + d.getAttributeValue("AXValue"), + h4s[0].getAttributeValue("AXValue"), + "Found correct h4 heading" + ); + + is( + j.getAttributeValue("AXValue"), + h4s[1].getAttributeValue("AXValue"), + "Found correct h4 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel5SearchKey"; + + let h5Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h5Count, "Found two h5 items"); + + let h5s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const e = getNativeInterface(accDoc, "e"); + const k = getNativeInterface(accDoc, "k"); + + is( + e.getAttributeValue("AXValue"), + h5s[0].getAttributeValue("AXValue"), + "Found correct h5 heading" + ); + + is( + k.getAttributeValue("AXValue"), + h5s[1].getAttributeValue("AXValue"), + "Found correct h5 heading" + ); + + searchPred.AXSearchKey = "AXHeadingLevel6SearchKey"; + + let h6Count = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, h6Count, "Found two h6 items"); + + let h6s = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const f = getNativeInterface(accDoc, "f"); + const l = getNativeInterface(accDoc, "l"); + + is( + f.getAttributeValue("AXValue"), + h6s[0].getAttributeValue("AXValue"), + "Found correct h6 heading" + ); + + is( + l.getAttributeValue("AXValue"), + h6s[1].getAttributeValue("AXValue"), + "Found correct h6 heading" + ); + } +); + +/* + * Test rotor with blockquotes + */ +addAccessibleTask( + ` + <blockquote id="first">hello I am a blockquote</blockquote> + <blockquote id="second"> + I am also a blockquote of the same level + <br> + <blockquote id="third">but I have a different level</blockquote> + </blockquote> + `, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXBlockquoteSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let bquotes = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(bquotes.length, 3, "Found three blockquotes"); + + const first = getNativeInterface(accDoc, "first"); + const second = getNativeInterface(accDoc, "second"); + const third = getNativeInterface(accDoc, "third"); + console.log("values :"); + console.log(first.getAttributeValue("AXValue")); + is( + first.getAttributeValue("AXValue"), + bquotes[0].getAttributeValue("AXValue"), + "Found correct first blockquote" + ); + + is( + second.getAttributeValue("AXValue"), + bquotes[1].getAttributeValue("AXValue"), + "Found correct second blockquote" + ); + + is( + third.getAttributeValue("AXValue"), + bquotes[2].getAttributeValue("AXValue"), + "Found correct third blockquote" + ); + } +); + +/* + * Test rotor with graphics + */ +addAccessibleTask( + ` + <img id="img1" alt="image one" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"><br> + <a href="http://example.com"> + <img id="img2" alt="image two" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> + </a> + <img src="" id="img3"> + `, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXGraphicSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let images = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(images.length, 3, "Found three images"); + + const img1 = getNativeInterface(accDoc, "img1"); + const img2 = getNativeInterface(accDoc, "img2"); + const img3 = getNativeInterface(accDoc, "img3"); + + is( + img1.getAttributeValue("AXDescription"), + images[0].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img2.getAttributeValue("AXDescription"), + images[1].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img3.getAttributeValue("AXDescription"), + images[2].getAttributeValue("AXDescription"), + "Found correct image" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_outline.js b/accessible/tests/browser/mac/browser_outline.js new file mode 100644 index 0000000000..1bb3c56bad --- /dev/null +++ b/accessible/tests/browser/mac/browser_outline.js @@ -0,0 +1,459 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test outline, outline rows with computed properties + */ +addAccessibleTask( + ` + <h3 id="tree1"> + Foods + </h3> + <ul role="tree" aria-labelledby="tree1" id="outline"> + <li role="treeitem" aria-expanded="false"> + <span> + Fruits + </span> + <ul> + <li role="none">Oranges</li> + <li role="treeitem" aria-expanded="true"> + <span> + Apples + </span> + <ul role="group"> + <li role="none">Honeycrisp</li> + <li role="none">Granny Smith</li> + </ul> + </li> + </ul> + </li> + <li id="vegetables" role="treeitem" aria-expanded="false"> + <span> + Vegetables + </span> + <ul role="group"> + <li role="treeitem" aria-expanded="true"> + <span> + Podded Vegetables + </span> + <ul role="group"> + <li role="none">Lentil</li> + <li role="none">Pea</li> + </ul> + </li> + </ul> + </li> + </ul> + `, + async (browser, accDoc) => { + const outline = getNativeInterface(accDoc, "outline"); + is( + outline.getAttributeValue("AXRole"), + "AXOutline", + "Correct role for outline" + ); + + const outChildren = outline.getAttributeValue("AXChildren"); + is(outChildren.length, 2, "Outline has two direct children"); + is(outChildren[0].getAttributeValue("AXSubrole"), "AXOutlineRow"); + is(outChildren[1].getAttributeValue("AXSubrole"), "AXOutlineRow"); + + const outRows = outline.getAttributeValue("AXRows"); + is(outRows.length, 4, "Outline has four rows"); + is( + outRows[0].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[0].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of outline" + ); + is( + outRows[0].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children, only group" + ); + is( + outRows[0].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is(outRows[1].getAttributeValue("AXDisclosing"), 1, "Row is disclosing"); + is( + outRows[1].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of group" + ); + is( + outRows[1].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children" + ); + is( + outRows[1].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is( + outRows[2].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[2].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of outline" + ); + is( + outRows[2].getAttributeValue("AXDisclosedRows").length, + 1, + "Row has one row child" + ); + is( + outRows[2].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is(outRows[3].getAttributeValue("AXDisclosing"), 1, "Row is disclosing"); + is( + outRows[3] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + outRows[2].getAttributeValue("AXDescription"), + "Row is direct child of row[2]" + ); + is( + outRows[3].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children" + ); + is( + outRows[3].getAttributeValue("AXDisclosureLevel"), + 1, + "Row is level one" + ); + + let evt = waitForMacEvent("AXRowExpanded", "vegetables"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("vegetables") + .setAttribute("aria-expanded", "true"); + }); + await evt; + is( + outRows[2].getAttributeValue("AXDisclosing"), + 1, + "Row is disclosing after being expanded" + ); + + evt = waitForMacEvent("AXRowCollapsed", "vegetables"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("vegetables") + .setAttribute("aria-expanded", "false"); + }); + await evt; + is( + outRows[2].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing after being collapsed again" + ); + } +); + +/** + * Test outline, outline rows with declared properties + */ +addAccessibleTask( + ` + <h3 id="tree1"> + Foods + </h3> + <ul role="tree" aria-labelledby="tree1" id="outline"> + <li role="treeitem" + aria-level="1" + aria-setsize="2" + aria-posinset="1" + aria-expanded="false"> + <span> + Fruits + </span> + <ul> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="1"> + Oranges + </li> + <li role="treeitem" + aria-level="2" + aria-setsize="2" + aria-posinset="2" + aria-expanded="true"> + <span> + Apples + </span> + <ul role="group"> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="1"> + Honeycrisp + </li> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="2"> + Granny Smith + </li> + </ul> + </li> + </ul> + </li> + <li role="treeitem" + aria-level="1" + aria-setsize="2" + aria-posinset="2" + aria-expanded="false"> + <span> + Vegetables + </span> + <ul role="group"> + <li role="treeitem" + aria-level="2" + aria-setsize="1" + aria-posinset="1" + aria-expanded="true"> + <span> + Podded Vegetables + </span> + <ul role="group"> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="1"> + Lentil + </li> + <li role="treeitem" + aria-level="3" + aria-setsize="2" + aria-posinset="2"> + Pea + </li> + </ul> + </li> + </ul> + </li> + </ul> + `, + async (browser, accDoc) => { + const outline = getNativeInterface(accDoc, "outline"); + is( + outline.getAttributeValue("AXRole"), + "AXOutline", + "Correct role for outline" + ); + + const outChildren = outline.getAttributeValue("AXChildren"); + is(outChildren.length, 2, "Outline has two direct children"); + is(outChildren[0].getAttributeValue("AXSubrole"), "AXOutlineRow"); + is(outChildren[1].getAttributeValue("AXSubrole"), "AXOutlineRow"); + + const outRows = outline.getAttributeValue("AXRows"); + is(outRows.length, 9, "Outline has nine rows"); + is( + outRows[0].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[0].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of outline" + ); + is( + outRows[0].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no direct row children, has list" + ); + is( + outRows[0].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is(outRows[2].getAttributeValue("AXDisclosing"), 1, "Row is disclosing"); + is( + outRows[2].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of group" + ); + is( + outRows[2].getAttributeValue("AXDisclosedRows").length, + 2, + "Row has two row children" + ); + is( + outRows[2].getAttributeValue("AXDisclosureLevel"), + 1, + "Row is level one" + ); + + is( + outRows[3].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[3] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + outRows[2].getAttributeValue("AXDescription"), + "Row is direct child of row 2" + ); + + is( + outRows[3].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children" + ); + is( + outRows[3].getAttributeValue("AXDisclosureLevel"), + 2, + "Row is level two" + ); + + is( + outRows[5].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[5].getAttributeValue("AXDisclosedByRow"), + null, + "Row is direct child of outline" + ); + is( + outRows[5].getAttributeValue("AXDisclosedRows").length, + 1, + "Row has no one row child" + ); + is( + outRows[5].getAttributeValue("AXDisclosureLevel"), + 0, + "Row is level zero" + ); + + is(outRows[6].getAttributeValue("AXDisclosing"), 1, "Row is disclosing"); + is( + outRows[6] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + outRows[5].getAttributeValue("AXDescription"), + "Row is direct child of row 5" + ); + is( + outRows[6].getAttributeValue("AXDisclosedRows").length, + 2, + "Row has two row children" + ); + is( + outRows[6].getAttributeValue("AXDisclosureLevel"), + 1, + "Row is level one" + ); + + is( + outRows[7].getAttributeValue("AXDisclosing"), + 0, + "Row is not disclosing" + ); + is( + outRows[7] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + outRows[6].getAttributeValue("AXDescription"), + "Row is direct child of row 6" + ); + is( + outRows[7].getAttributeValue("AXDisclosedRows").length, + 0, + "Row has no row children" + ); + is( + outRows[7].getAttributeValue("AXDisclosureLevel"), + 2, + "Row is level two" + ); + } +); + +// Test outline that isn't built with li/uls gets correct desc +addAccessibleTask( + ` + <div role="tree" id="tree" tabindex="0" aria-label="My drive" aria-activedescendant="myfiles"> + <div id="myfiles" role="treeitem" aria-label="My files" aria-selected="true" aria-expanded="false">My files</div> + <div role="treeitem" aria-label="Shared items" aria-selected="false" aria-expanded="false">Shared items</div> + </div> + `, + async (browser, accDoc) => { + const tree = getNativeInterface(accDoc, "tree"); + is(tree.getAttributeValue("AXRole"), "AXOutline", "Correct role for tree"); + + const treeItems = tree.getAttributeValue("AXChildren"); + is(treeItems.length, 2, "Outline has two direct children"); + is(treeItems[0].getAttributeValue("AXSubrole"), "AXOutlineRow"); + is(treeItems[1].getAttributeValue("AXSubrole"), "AXOutlineRow"); + + const outRows = tree.getAttributeValue("AXRows"); + is(outRows.length, 2, "Outline has two rows"); + + is( + outRows[0].getAttributeValue("AXDescription"), + "My files", + "files labelled correctly" + ); + is( + outRows[1].getAttributeValue("AXDescription"), + "Shared items", + "shared items labelled correctly" + ); + } +); + +// Test outline registers AXDisclosed attr as settable +addAccessibleTask( + ` + <div role="tree" id="tree" tabindex="0" aria-label="My drive" aria-activedescendant="myfiles"> + <div id="myfiles" role="treeitem" aria-label="My files" aria-selected="true" aria-expanded="false">My files</div> + <div role="treeitem" aria-label="Shared items" aria-selected="false" aria-expanded="true">Shared items</div> + </div> + `, + async (browser, accDoc) => { + const tree = getNativeInterface(accDoc, "tree"); + const treeItems = tree.getAttributeValue("AXChildren"); + + is(treeItems.length, 2, "Outline has two direct children"); + is(treeItems[0].getAttributeValue("AXDisclosing"), 0); + is(treeItems[1].getAttributeValue("AXDisclosing"), 1); + + is(treeItems[0].isAttributeSettable("AXDisclosing"), true); + is(treeItems[1].isAttributeSettable("AXDisclosing"), true); + + // attempt to change attribute values + treeItems[0].setAttributeValue("AXDisclosing", 1); + treeItems[0].setAttributeValue("AXDisclosing", 0); + + // verify they're unchanged + is(treeItems[0].getAttributeValue("AXDisclosing"), 0); + is(treeItems[1].getAttributeValue("AXDisclosing"), 1); + } +); diff --git a/accessible/tests/browser/mac/browser_outline_xul.js b/accessible/tests/browser/mac/browser_outline_xul.js new file mode 100644 index 0000000000..66eebebf50 --- /dev/null +++ b/accessible/tests/browser/mac/browser_outline_xul.js @@ -0,0 +1,274 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/attributes.js */ +loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + "mac/doc_tree.xhtml", + async (browser, accDoc) => { + const tree = getNativeInterface(accDoc, "tree"); + is( + tree.getAttributeValue("AXRole"), + "AXOutline", + "Found tree with role outline" + ); + // XUL trees store all rows as direct children of the outline, + // so we should see nine here instead of just three: + // (Groceries, Fruits, Veggies) + const treeChildren = tree.getAttributeValue("AXChildren"); + is(treeChildren.length, 9, "Found nine direct children"); + + const treeCols = tree.getAttributeValue("AXColumns"); + is(treeCols.length, 1, "Found one column in tree"); + + // Here, we should get only outline rows, not the title + const treeRows = tree.getAttributeValue("AXRows"); + is(treeRows.length, 8, "Found 8 total rows"); + + is( + treeRows[0].getAttributeValue("AXDescription"), + "Fruits", + "Located correct first row, row has correct desc" + ); + is( + treeRows[0].getAttributeValue("AXDisclosing"), + 1, + "Fruits is disclosing" + ); + is( + treeRows[0].getAttributeValue("AXDisclosedByRow"), + null, + "Fruits is disclosed by outline" + ); + is( + treeRows[0].getAttributeValue("AXDisclosureLevel"), + 0, + "Fruits is level zero" + ); + let disclosedRows = treeRows[0].getAttributeValue("AXDisclosedRows"); + is(disclosedRows.length, 2, "Fruits discloses two rows"); + is( + disclosedRows[0].getAttributeValue("AXDescription"), + "Apple", + "fruits discloses apple" + ); + is( + disclosedRows[1].getAttributeValue("AXDescription"), + "Orange", + "fruits discloses orange" + ); + + is( + treeRows[1].getAttributeValue("AXDescription"), + "Apple", + "Located correct second row, row has correct desc" + ); + is( + treeRows[1].getAttributeValue("AXDisclosing"), + 0, + "Apple is not disclosing" + ); + is( + treeRows[1] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Fruits", + "Apple is disclosed by fruits" + ); + is( + treeRows[1].getAttributeValue("AXDisclosureLevel"), + 1, + "Apple is level one" + ); + is( + treeRows[1].getAttributeValue("AXDisclosedRows").length, + 0, + "Apple does not disclose rows" + ); + + is( + treeRows[2].getAttributeValue("AXDescription"), + "Orange", + "Located correct third row, row has correct desc" + ); + is( + treeRows[2].getAttributeValue("AXDisclosing"), + 0, + "Orange is not disclosing" + ); + is( + treeRows[2] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Fruits", + "Orange is disclosed by fruits" + ); + is( + treeRows[2].getAttributeValue("AXDisclosureLevel"), + 1, + "Orange is level one" + ); + is( + treeRows[2].getAttributeValue("AXDisclosedRows").length, + 0, + "Orange does not disclose rows" + ); + + is( + treeRows[3].getAttributeValue("AXDescription"), + "Veggies", + "Located correct fourth row, row has correct desc" + ); + is( + treeRows[3].getAttributeValue("AXDisclosing"), + 1, + "Veggies is disclosing" + ); + is( + treeRows[3].getAttributeValue("AXDisclosedByRow"), + null, + "Veggies is disclosed by outline" + ); + is( + treeRows[3].getAttributeValue("AXDisclosureLevel"), + 0, + "Veggies is level zero" + ); + disclosedRows = treeRows[3].getAttributeValue("AXDisclosedRows"); + is(disclosedRows.length, 2, "Veggies discloses two rows"); + is( + disclosedRows[0].getAttributeValue("AXDescription"), + "Green Veggies", + "Veggies discloses green veggies" + ); + is( + disclosedRows[1].getAttributeValue("AXDescription"), + "Squash", + "Veggies discloses squash" + ); + + is( + treeRows[4].getAttributeValue("AXDescription"), + "Green Veggies", + "Located correct fifth row, row has correct desc" + ); + is( + treeRows[4].getAttributeValue("AXDisclosing"), + 1, + "Green veggies is disclosing" + ); + is( + treeRows[4] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Veggies", + "Green Veggies is disclosed by veggies" + ); + is( + treeRows[4].getAttributeValue("AXDisclosureLevel"), + 1, + "Green veggies is level one" + ); + disclosedRows = treeRows[4].getAttributeValue("AXDisclosedRows"); + is(disclosedRows.length, 2, "Green veggies has two rows"); + is( + disclosedRows[0].getAttributeValue("AXDescription"), + "Spinach", + "Green veggies discloses spinach" + ); + is( + disclosedRows[1].getAttributeValue("AXDescription"), + "Peas", + "Green veggies discloses peas" + ); + + is( + treeRows[5].getAttributeValue("AXDescription"), + "Spinach", + "Located correct sixth row, row has correct desc" + ); + is( + treeRows[5].getAttributeValue("AXDisclosing"), + 0, + "Spinach is not disclosing" + ); + is( + treeRows[5] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Green Veggies", + "Spinach is disclosed by green veggies" + ); + is( + treeRows[5].getAttributeValue("AXDisclosureLevel"), + 2, + "Spinach is level two" + ); + is( + treeRows[5].getAttributeValue("AXDisclosedRows").length, + 0, + "Spinach does not disclose rows" + ); + + is( + treeRows[6].getAttributeValue("AXDescription"), + "Peas", + "Located correct seventh row, row has correct desc" + ); + is( + treeRows[6].getAttributeValue("AXDisclosing"), + 0, + "Peas is not disclosing" + ); + is( + treeRows[6] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Green Veggies", + "Peas is disclosed by green veggies" + ); + is( + treeRows[6].getAttributeValue("AXDisclosureLevel"), + 2, + "Peas is level two" + ); + is( + treeRows[6].getAttributeValue("AXDisclosedRows").length, + 0, + "Peas does not disclose rows" + ); + + is( + treeRows[7].getAttributeValue("AXDescription"), + "Squash", + "Located correct eighth row, row has correct desc" + ); + is( + treeRows[7].getAttributeValue("AXDisclosing"), + 0, + "Squash is not disclosing" + ); + is( + treeRows[7] + .getAttributeValue("AXDisclosedByRow") + .getAttributeValue("AXDescription"), + "Veggies", + "Squash is disclosed by veggies" + ); + is( + treeRows[7].getAttributeValue("AXDisclosureLevel"), + 1, + "Squash is level one" + ); + is( + treeRows[7].getAttributeValue("AXDisclosedRows").length, + 0, + "Squash does not disclose rows" + ); + }, + { topLevel: false, chrome: true } +); diff --git a/accessible/tests/browser/mac/browser_popupbutton.js b/accessible/tests/browser/mac/browser_popupbutton.js new file mode 100644 index 0000000000..9916a7cb12 --- /dev/null +++ b/accessible/tests/browser/mac/browser_popupbutton.js @@ -0,0 +1,153 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +// Test dropdown select element +addAccessibleTask( + `<select id="select" aria-label="Choose a number"> + <option id="one" selected>One</option> + <option id="two">Two</option> + <option id="three">Three</option> + <option id="four" disabled>Four</option> + </select>`, + async (browser, accDoc) => { + // Test combobox + let select = getNativeInterface(accDoc, "select"); + is( + select.getAttributeValue("AXRole"), + "AXPopUpButton", + "select has AXPopupButton role" + ); + ok(select.attributeNames.includes("AXValue"), "select advertises AXValue"); + is( + select.getAttributeValue("AXValue"), + "One", + "select has correctt initial value" + ); + ok( + !select.attributeNames.includes("AXHasPopup"), + "select does not advertise AXHasPopup" + ); + is( + select.getAttributeValue("AXHasPopup"), + null, + "select does not provide value for AXHasPopup" + ); + + ok(select.actionNames.includes("AXPress"), "Selectt has press action"); + // These three events happen in quick succession when select is pressed + let events = Promise.all([ + waitForMacEvent("AXMenuOpened"), + waitForMacEvent("AXSelectedChildrenChanged"), + waitForMacEvent("AXFocusedUIElementChanged"), + ]); + select.performAction("AXPress"); + // Only capture the target of AXMenuOpened (first element) + let [menu] = await events; + + is(menu.getAttributeValue("AXRole"), "AXMenu", "dropdown has AXMenu role"); + is( + menu.getAttributeValue("AXSelectedChildren").length, + 1, + "dropdown has single selected child" + ); + + let selectedChildren = menu.getAttributeValue("AXSelectedChildren"); + is(selectedChildren.length, 1, "Only one child is selected"); + is(selectedChildren[0].getAttributeValue("AXRole"), "AXMenuItem"); + is(selectedChildren[0].getAttributeValue("AXTitle"), "One"); + + let menuParent = menu.getAttributeValue("AXParent"); + is( + menuParent.getAttributeValue("AXRole"), + "AXPopUpButton", + "dropdown parent is a popup button" + ); + + let menuItems = menu.getAttributeValue("AXChildren").map(c => { + return [ + c.getAttributeValue("AXMenuItemMarkChar"), + c.getAttributeValue("AXRole"), + c.getAttributeValue("AXTitle"), + c.getAttributeValue("AXEnabled"), + ]; + }); + + Assert.deepEqual( + menuItems, + [ + ["✓", "AXMenuItem", "One", true], + [null, "AXMenuItem", "Two", true], + [null, "AXMenuItem", "Three", true], + [null, "AXMenuItem", "Four", false], + ], + "Menu items have correct checkmark on current value, correctt roles, correct titles, and correct AXEnabled value" + ); + + events = Promise.all([ + waitForMacEvent("AXSelectedChildrenChanged"), + waitForMacEvent("AXFocusedUIElementChanged"), + ]); + EventUtils.synthesizeKey("KEY_ArrowDown"); + let [, menuItem] = await events; + is( + menuItem.getAttributeValue("AXTitle"), + "Two", + "Focused menu item has correct title" + ); + + selectedChildren = menu.getAttributeValue("AXSelectedChildren"); + is(selectedChildren.length, 1, "Only one child is selected"); + is( + selectedChildren[0].getAttributeValue("AXTitle"), + "Two", + "Selected child matches focused item" + ); + + events = Promise.all([ + waitForMacEvent("AXSelectedChildrenChanged"), + waitForMacEvent("AXFocusedUIElementChanged"), + ]); + EventUtils.synthesizeKey("KEY_ArrowDown"); + [, menuItem] = await events; + is( + menuItem.getAttributeValue("AXTitle"), + "Three", + "Focused menu item has correct title" + ); + + selectedChildren = menu.getAttributeValue("AXSelectedChildren"); + is(selectedChildren.length, 1, "Only one child is selected"); + is( + selectedChildren[0].getAttributeValue("AXTitle"), + "Three", + "Selected child matches focused item" + ); + + events = Promise.all([ + waitForMacEvent("AXMenuClosed"), + waitForMacEvent("AXFocusedUIElementChanged"), + ]); + menuItem.performAction("AXPress"); + let [, newFocus] = await events; + is( + newFocus.getAttributeValue("AXRole"), + "AXPopUpButton", + "Newly focused element is AXPopupButton" + ); + is( + newFocus.getAttributeValue("AXValue"), + "Three", + "select has correct new value" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_radio_position.js b/accessible/tests/browser/mac/browser_radio_position.js new file mode 100644 index 0000000000..76f518a91e --- /dev/null +++ b/accessible/tests/browser/mac/browser_radio_position.js @@ -0,0 +1,321 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function getChildRoles(parent) { + return parent + .getAttributeValue("AXChildren") + .map(c => c.getAttributeValue("AXRole")); +} + +function getLinkedTitles(element) { + return element + .getAttributeValue("AXLinkedUIElements") + .map(c => c.getAttributeValue("AXTitle")); +} + +/** + * Test radio group + */ +addAccessibleTask( + `<div role="radiogroup" id="radioGroup"> + <div role="radio" + id="radioGroupItem1"> + Regular crust + </div> + <div role="radio" + id="radioGroupItem2"> + Deep dish + </div> + <div role="radio" + id="radioGroupItem3"> + Thin crust + </div> + </div>`, + async (browser, accDoc) => { + let item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let item2 = getNativeInterface(accDoc, "radioGroupItem2"); + let item3 = getNativeInterface(accDoc, "radioGroupItem3"); + let titleList = ["Regular crust", "Deep dish", "Thin crust"]; + + Assert.deepEqual( + titleList, + [item1, item2, item3].map(c => c.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 1 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item1), + titleList, + "Item one has correctly ordered linked elements" + ); + + linkedElems = item2.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 2 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item2), + titleList, + "Item two has correctly ordered linked elements" + ); + + linkedElems = item3.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Item 3 has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item3), + titleList, + "Item three has correctly ordered linked elements" + ); + } +); + +/** + * Test dynamic add to a radio group + */ +addAccessibleTask( + `<div role="radiogroup" id="radioGroup"> + <div role="radio" + id="radioGroupItem1"> + Option One + </div> + </div>`, + async (browser, accDoc) => { + let item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + + is(linkedElems.length, 1, "Item 1 has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + item1.getAttributeValue("AXTitle"), + "Item 1 is first element" + ); + + let reorder = waitForEvent(EVENT_REORDER, "radioGroup"); + await SpecialPowers.spawn(browser, [], () => { + let d = content.document.createElement("div"); + d.setAttribute("role", "radio"); + content.document.getElementById("radioGroup").appendChild(d); + }); + await reorder; + + let radioGroup = getNativeInterface(accDoc, "radioGroup"); + let groupMembers = radioGroup.getAttributeValue("AXChildren"); + is(groupMembers.length, 2, "Radio group has two members"); + let item2 = groupMembers[1]; + item1 = getNativeInterface(accDoc, "radioGroupItem1"); + let titleList = ["Option One", ""]; + + Assert.deepEqual( + titleList, + [item1, item2].map(c => c.getAttributeValue("AXTitle")), + "Title list matches" + ); + + linkedElems = item1.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Item 1 has two linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item1), + titleList, + "Item one has correctly ordered linked elements" + ); + + linkedElems = item2.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Item 2 has two linked UI elems"); + Assert.deepEqual( + getLinkedTitles(item2), + titleList, + "Item two has correctly ordered linked elements" + ); + } +); + +/** + * Test input[type=radio] for single group + */ +addAccessibleTask( + `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label> + <input type="radio" id="dog" name="animal"><label for="dog">Dog</label> + <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label>`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + let titleList = ["Cat", "Dog", "CatDog"]; + + Assert.deepEqual( + titleList, + [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Cat has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(cat), + titleList, + "Cat has correctly ordered linked elements" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Dog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(dog), + titleList, + "Dog has correctly ordered linked elements" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Catdog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(catdog), + titleList, + "catdog has correctly ordered linked elements" + ); + } +); + +/** + * Test input[type=radio] for different groups + */ +addAccessibleTask( + `<input type="radio" id="cat" name="one"><label for="cat">Cat</label> + <input type="radio" id="dog" name="two"><label for="dog">Dog</label> + <input type="radio" id="catdog"><label for="catdog">CatDog</label>`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Cat has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + cat.getAttributeValue("AXTitle"), + "Cat is only element" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Dog has one linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + dog.getAttributeValue("AXTitle"), + "Dog is only element" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 0, "Catdog has no linked UI elem"); + } +); + +/** + * Test input[type=radio] for single group across DOM + */ +addAccessibleTask( + `<input type="radio" id="cat" name="animal"><label for="cat">Cat</label> + <div> + <span> + <input type="radio" id="dog" name="animal"><label for="dog">Dog</label> + </span> + </div> + <div> + <input type="radio" id="catdog" name="animal"><label for="catdog">CatDog</label> + </div>`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let dog = getNativeInterface(accDoc, "dog"); + let catdog = getNativeInterface(accDoc, "catdog"); + let titleList = ["Cat", "Dog", "CatDog"]; + + Assert.deepEqual( + titleList, + [cat, dog, catdog].map(x => x.getAttributeValue("AXTitle")), + "Title list matches" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Cat has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(cat), + titleList, + "cat has correctly ordered linked elements" + ); + + linkedElems = dog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Dog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(dog), + titleList, + "dog has correctly ordered linked elements" + ); + + linkedElems = catdog.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 3, "Catdog has three linked UI elems"); + Assert.deepEqual( + getLinkedTitles(catdog), + titleList, + "catdog has correctly ordered linked elements" + ); + } +); + +/** + * Test dynamic add of input[type=radio] in a single group + */ +addAccessibleTask( + `<div id="container"><input type="radio" id="cat" name="animal"></div>`, + async (browser, accDoc) => { + let cat = getNativeInterface(accDoc, "cat"); + let container = getNativeInterface(accDoc, "container"); + + let containerChildren = container.getAttributeValue("AXChildren"); + is(containerChildren.length, 1, "container has one button"); + is( + containerChildren[0].getAttributeValue("AXRole"), + "AXRadioButton", + "Container child is radio button" + ); + + let linkedElems = cat.getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 1, "Cat has 1 linked UI elem"); + is( + linkedElems[0].getAttributeValue("AXTitle"), + cat.getAttributeValue("AXTitle"), + "Cat is first element" + ); + let reorder = waitForEvent(EVENT_REORDER, "container"); + await SpecialPowers.spawn(browser, [], () => { + let input = content.document.createElement("input"); + input.setAttribute("type", "radio"); + input.setAttribute("name", "animal"); + content.document.getElementById("container").appendChild(input); + }); + await reorder; + + container = getNativeInterface(accDoc, "container"); + containerChildren = container.getAttributeValue("AXChildren"); + + is(containerChildren.length, 2, "container has two children"); + + Assert.deepEqual( + getChildRoles(container), + ["AXRadioButton", "AXRadioButton"], + "Both children are radio buttons" + ); + + linkedElems = containerChildren[0].getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "Cat has 2 linked elements"); + + linkedElems = containerChildren[1].getAttributeValue("AXLinkedUIElements"); + is(linkedElems.length, 2, "New button has 2 linked elements"); + } +); diff --git a/accessible/tests/browser/mac/browser_range.js b/accessible/tests/browser/mac/browser_range.js new file mode 100644 index 0000000000..7158dfcb30 --- /dev/null +++ b/accessible/tests/browser/mac/browser_range.js @@ -0,0 +1,92 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test input[type=range] + */ +addAccessibleTask( + `<input id="range" type="range" min="1" max="100" value="1" step="10">`, + async (browser, accDoc) => { + let range = getNativeInterface(accDoc, "range"); + is(range.getAttributeValue("AXRole"), "AXSlider", "Correct AXSlider role"); + is(range.getAttributeValue("AXValue"), "1", "Correct initial value"); + + let actions = range.actionNames; + ok(actions.includes("AXDecrement"), "Has decrement action"); + ok(actions.includes("AXIncrement"), "Has increment action"); + + let evt = waitForMacEvent("AXValueChanged"); + range.performAction("AXIncrement"); + await evt; + is(range.getAttributeValue("AXValue"), "11", "Correct increment value"); + + evt = waitForMacEvent("AXValueChanged"); + range.performAction("AXDecrement"); + await evt; + is(range.getAttributeValue("AXValue"), "1", "Correct decrement value"); + + evt = waitForMacEvent("AXValueChanged"); + // Adjust value via script in content + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("range").value = 41; + }); + await evt; + is( + range.getAttributeValue("AXValue"), + "41", + "Correct value from content change" + ); + } +); + +/** + * Test input[type=range] + */ +addAccessibleTask( + `<input type="number" value="11" id="number">`, + async (browser, accDoc) => { + let number = getNativeInterface(accDoc, "number"); + is( + number.getAttributeValue("AXRole"), + "AXIncrementor", + "Correct AXIncrementor role" + ); + is(number.getAttributeValue("AXValue"), "11", "Correct initial value"); + + let actions = number.actionNames; + ok(actions.includes("AXDecrement"), "Has decrement action"); + ok(actions.includes("AXIncrement"), "Has increment action"); + + let evt = waitForMacEvent("AXValueChanged"); + number.performAction("AXIncrement"); + await evt; + is(number.getAttributeValue("AXValue"), "12", "Correct increment value"); + + evt = waitForMacEvent("AXValueChanged"); + number.performAction("AXDecrement"); + await evt; + is(number.getAttributeValue("AXValue"), "11", "Correct decrement value"); + + evt = waitForMacEvent("AXValueChanged"); + // Adjust value via script in content + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("number").value = 42; + }); + await evt; + is( + number.getAttributeValue("AXValue"), + "42", + "Correct value from content change" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_required.js b/accessible/tests/browser/mac/browser_required.js new file mode 100644 index 0000000000..5f552d44c3 --- /dev/null +++ b/accessible/tests/browser/mac/browser_required.js @@ -0,0 +1,145 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test required and aria-required attributes on checkboxes + * and radio buttons. + */ +addAccessibleTask( + ` + <form> + <input type="checkbox" id="checkbox" required> + <br> + <input type="radio" id="radio" required> + <br> + <input type="checkbox" id="ariaCheckbox" aria-required="true"> + <br> + <input type="radio" id="ariaRadio" aria-required="true"> + </form> + `, + async (browser, accDoc) => { + // Check initial AXRequired values are correct + let radio = getNativeInterface(accDoc, "radio"); + is( + radio.getAttributeValue("AXRequired"), + 1, + "Correct required val for radio" + ); + + let ariaRadio = getNativeInterface(accDoc, "ariaRadio"); + is( + ariaRadio.getAttributeValue("AXRequired"), + 1, + "Correct required val for ariaRadio" + ); + + let checkbox = getNativeInterface(accDoc, "checkbox"); + is( + checkbox.getAttributeValue("AXRequired"), + 1, + "Correct required val for checkbox" + ); + + let ariaCheckbox = getNativeInterface(accDoc, "ariaCheckbox"); + is( + ariaCheckbox.getAttributeValue("AXRequired"), + 1, + "Correct required val for ariaCheckbox" + ); + + // Change aria-required, verify AXRequired is updated + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaCheckbox") + .setAttribute("aria-required", "false"); + }); + await stateChanged; + + is( + ariaCheckbox.getAttributeValue("AXRequired"), + 0, + "Correct required after false set for ariaCheckbox" + ); + + // Remove aria-required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaCheckbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaCheckbox") + .removeAttribute("aria-required"); + }); + await stateChanged; + + is( + ariaCheckbox.getAttributeValue("AXRequired"), + 0, + "Correct required after removal for ariaCheckbox" + ); + + // Change aria-required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaRadio") + .setAttribute("aria-required", "false"); + }); + await stateChanged; + + is( + ariaRadio.getAttributeValue("AXRequired"), + 0, + "Correct required after false set for ariaRadio" + ); + + // Remove aria-required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "ariaRadio"); + await SpecialPowers.spawn(browser, [], () => { + content.document + .getElementById("ariaRadio") + .removeAttribute("aria-required"); + }); + await stateChanged; + + is( + ariaRadio.getAttributeValue("AXRequired"), + 0, + "Correct required after removal for ariaRadio" + ); + + // Remove required, verify AXRequired is updated + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "checkbox"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("checkbox").removeAttribute("required"); + }); + await stateChanged; + + is( + checkbox.getAttributeValue("AXRequired"), + 0, + "Correct required after removal for checkbox" + ); + + stateChanged = waitForEvent(EVENT_STATE_CHANGE, "radio"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("radio").removeAttribute("required"); + }); + await stateChanged; + + is( + checkbox.getAttributeValue("AXRequired"), + 0, + "Correct required after removal for radio" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_rich_listbox.js b/accessible/tests/browser/mac/browser_rich_listbox.js new file mode 100644 index 0000000000..97dd6785bb --- /dev/null +++ b/accessible/tests/browser/mac/browser_rich_listbox.js @@ -0,0 +1,73 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/attributes.js */ +loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); + +addAccessibleTask( + "mac/doc_rich_listbox.xhtml", + async (browser, accDoc) => { + const categories = getNativeInterface(accDoc, "categories"); + const categoriesChildren = categories.getAttributeValue("AXChildren"); + is(categoriesChildren.length, 4, "Found listbox and 4 items"); + + const general = getNativeInterface(accDoc, "general"); + is( + general.getAttributeValue("AXTitle"), + "general", + "general has appropriate title" + ); + is( + categoriesChildren[0].getAttributeValue("AXTitle"), + general.getAttributeValue("AXTitle"), + "Found general listitem" + ); + is( + general.getAttributeValue("AXEnabled"), + 1, + "general is enabled, not dimmed" + ); + + const home = getNativeInterface(accDoc, "home"); + is(home.getAttributeValue("AXTitle"), "home", "home has appropriate title"); + is( + categoriesChildren[1].getAttributeValue("AXTitle"), + home.getAttributeValue("AXTitle"), + "Found home listitem" + ); + is(home.getAttributeValue("AXEnabled"), 1, "Home is enabled, not dimmed"); + + const search = getNativeInterface(accDoc, "search"); + is( + search.getAttributeValue("AXTitle"), + "search", + "search has appropriate title" + ); + is( + categoriesChildren[2].getAttributeValue("AXTitle"), + search.getAttributeValue("AXTitle"), + "Found search listitem" + ); + is( + search.getAttributeValue("AXEnabled"), + 1, + "search is enabled, not dimmed" + ); + + const privacy = getNativeInterface(accDoc, "privacy"); + is( + privacy.getAttributeValue("AXTitle"), + "privacy", + "privacy has appropriate title" + ); + is( + categoriesChildren[3].getAttributeValue("AXTitle"), + privacy.getAttributeValue("AXTitle"), + "Found privacy listitem" + ); + }, + { topLevel: false, chrome: true } +); diff --git a/accessible/tests/browser/mac/browser_roles_elements.js b/accessible/tests/browser/mac/browser_roles_elements.js new file mode 100644 index 0000000000..5f07961486 --- /dev/null +++ b/accessible/tests/browser/mac/browser_roles_elements.js @@ -0,0 +1,309 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test different HTML elements for their roles and subroles + */ +function testRoleAndSubRole(accDoc, id, axRole, axSubRole, axRoleDescription) { + let el = getNativeInterface(accDoc, id); + if (axRole) { + is( + el.getAttributeValue("AXRole"), + axRole, + "AXRole for " + id + " is " + axRole + ); + } + if (axSubRole) { + is( + el.getAttributeValue("AXSubrole"), + axSubRole, + "Subrole for " + id + " is " + axSubRole + ); + } + if (axRoleDescription) { + is( + el.getAttributeValue("AXRoleDescription"), + axRoleDescription, + "Subrole for " + id + " is " + axRoleDescription + ); + } +} + +addAccessibleTask( + ` + <!-- WAI-ARIA landmark roles --> + <div id="application" role="application"></div> + <div id="banner" role="banner"></div> + <div id="complementary" role="complementary"></div> + <div id="contentinfo" role="contentinfo"></div> + <div id="form" role="form"></div> + <div id="main" role="main"></div> + <div id="navigation" role="navigation"></div> + <div id="search" role="search"></div> + <div id="searchbox" role="searchbox"></div> + + <!-- DPub landmarks --> + <div id="dPubNavigation" role="doc-index"></div> + <div id="dPubRegion" role="doc-introduction"></div> + + <!-- Other WAI-ARIA widget roles --> + <div id="alert" role="alert"></div> + <div id="alertdialog" role="alertdialog"></div> + <div id="article" role="article"></div> + <div id="code" role="code"></div> + <div id="dialog" role="dialog"></div> + <div id="ariaDocument" role="document"></div> + <div id="log" role="log"></div> + <div id="marquee" role="marquee"></div> + <div id="ariaMath" role="math"></div> + <div id="note" role="note"></div> + <div id="ariaRegion" aria-label="region" role="region"></div> + <div id="ariaStatus" role="status"></div> + <div id="switch" role="switch"></div> + <div id="timer" role="timer"></div> + <div id="tooltip" role="tooltip"></div> + + <!-- text entries --> + <div id="textbox_multiline" role="textbox" aria-multiline="true"></div> + <div id="textbox_singleline" role="textbox" aria-multiline="false"></div> + <textarea id="textArea"></textarea> + <input id="textInput"> + + <!-- True HTML5 search box --> + <input type="search" id="htmlSearch" /> + + <!-- A button morphed into a toggle via ARIA --> + <button id="toggle" aria-pressed="false"></button> + + <!-- A button with a 'banana' role description --> + <button id="banana" aria-roledescription="banana"></button> + + <!-- Other elements --> + <del id="deletion">Deleted text</del> + <dl id="dl"><dt id="dt">term</dt><dd id="dd">definition</dd></dl> + <hr id="hr" /> + <ins id="insertion">Inserted text</ins> + + <!-- Some SVG stuff --> + <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg" + xmlns:xlink="http://www.w3.org/1999/xlink"> + <g id="g"> + <title>g</title> + </g> + <rect width="300" height="100" id="rect" + style="fill:rgb(0,0,255);stroke-width:1;stroke:rgb(0,0,0)"> + <title>rect</title> + </rect> + <circle cx="100" cy="50" r="40" stroke="black" id="circle" + stroke-width="2" fill="red"> + <title>circle</title> + </circle> + <ellipse cx="300" cy="80" rx="100" ry="50" id="ellipse" + style="fill:yellow;stroke:purple;stroke-width:2"> + <title>ellipse</title> + </ellipse> + <line x1="0" y1="0" x2="200" y2="200" id="line" + style="stroke:rgb(255,0,0);stroke-width:2"> + <title>line</title> + </line> + <polygon points="200,10 250,190 160,210" id="polygon" + style="fill:lime;stroke:purple;stroke-width:1"> + <title>polygon</title> + </polygon> + <polyline points="20,20 40,25 60,40 80,120 120,140 200,180" id="polyline" + style="fill:none;stroke:black;stroke-width:3" > + <title>polyline</title> + </polyline> + <path d="M150 0 L75 200 L225 200 Z" id="path"> + <title>path</title> + </path> + <image x1="25" y1="80" width="50" height="20" id="image" + xlink:href="../moz.png"> + <title>image</title> + </image> + </svg>`, + (browser, accDoc) => { + // WAI-ARIA landmark subroles, regardless of AXRole + testRoleAndSubRole(accDoc, "application", null, "AXLandmarkApplication"); + testRoleAndSubRole(accDoc, "banner", null, "AXLandmarkBanner"); + testRoleAndSubRole( + accDoc, + "complementary", + null, + "AXLandmarkComplementary" + ); + testRoleAndSubRole(accDoc, "contentinfo", null, "AXLandmarkContentInfo"); + testRoleAndSubRole(accDoc, "form", null, "AXLandmarkForm"); + testRoleAndSubRole(accDoc, "main", null, "AXLandmarkMain"); + testRoleAndSubRole(accDoc, "navigation", null, "AXLandmarkNavigation"); + testRoleAndSubRole(accDoc, "search", null, "AXLandmarkSearch"); + testRoleAndSubRole(accDoc, "searchbox", null, "AXSearchField"); + + // DPub roles map into two categories, sample one of each + testRoleAndSubRole( + accDoc, + "dPubNavigation", + "AXGroup", + "AXLandmarkNavigation" + ); + testRoleAndSubRole(accDoc, "dPubRegion", "AXGroup", "AXLandmarkRegion"); + + // ARIA widget roles + testRoleAndSubRole(accDoc, "alert", null, "AXApplicationAlert"); + testRoleAndSubRole( + accDoc, + "alertdialog", + "AXGroup", + "AXApplicationAlertDialog", + "alert dialog" + ); + testRoleAndSubRole(accDoc, "article", null, "AXDocumentArticle"); + testRoleAndSubRole(accDoc, "code", "AXGroup", "AXCodeStyleGroup"); + testRoleAndSubRole(accDoc, "dialog", null, "AXApplicationDialog", "dialog"); + testRoleAndSubRole(accDoc, "ariaDocument", null, "AXDocument"); + testRoleAndSubRole(accDoc, "log", null, "AXApplicationLog"); + testRoleAndSubRole(accDoc, "marquee", null, "AXApplicationMarquee"); + testRoleAndSubRole(accDoc, "ariaMath", null, "AXDocumentMath"); + testRoleAndSubRole(accDoc, "note", null, "AXDocumentNote"); + testRoleAndSubRole(accDoc, "ariaRegion", null, "AXLandmarkRegion"); + testRoleAndSubRole(accDoc, "ariaStatus", "AXGroup", "AXApplicationStatus"); + testRoleAndSubRole(accDoc, "switch", "AXCheckBox", "AXSwitch"); + testRoleAndSubRole(accDoc, "timer", null, "AXApplicationTimer"); + testRoleAndSubRole(accDoc, "tooltip", "AXGroup", "AXUserInterfaceTooltip"); + + // Text boxes + testRoleAndSubRole(accDoc, "textbox_multiline", "AXTextArea"); + testRoleAndSubRole(accDoc, "textbox_singleline", "AXTextArea"); + testRoleAndSubRole(accDoc, "textArea", "AXTextArea"); + testRoleAndSubRole(accDoc, "textInput", "AXTextField"); + + // True HTML5 search field + testRoleAndSubRole(accDoc, "htmlSearch", "AXTextField", "AXSearchField"); + + // A button morphed into a toggle by ARIA + testRoleAndSubRole(accDoc, "toggle", "AXCheckBox", "AXToggle"); + + // A banana button + testRoleAndSubRole(accDoc, "banana", "AXButton", null, "banana"); + + // Other elements + testRoleAndSubRole(accDoc, "deletion", "AXGroup", "AXDeleteStyleGroup"); + testRoleAndSubRole(accDoc, "dl", "AXList", "AXDescriptionList"); + testRoleAndSubRole(accDoc, "dt", "AXGroup", "AXTerm"); + testRoleAndSubRole(accDoc, "dd", "AXGroup", "AXDescription"); + testRoleAndSubRole(accDoc, "hr", "AXSplitter", "AXContentSeparator"); + testRoleAndSubRole(accDoc, "insertion", "AXGroup", "AXInsertStyleGroup"); + + // Some SVG stuff + testRoleAndSubRole(accDoc, "svg", "AXImage"); + testRoleAndSubRole(accDoc, "g", "AXGroup"); + testRoleAndSubRole(accDoc, "rect", "AXImage"); + testRoleAndSubRole(accDoc, "circle", "AXImage"); + testRoleAndSubRole(accDoc, "ellipse", "AXImage"); + testRoleAndSubRole(accDoc, "line", "AXImage"); + testRoleAndSubRole(accDoc, "polygon", "AXImage"); + testRoleAndSubRole(accDoc, "polyline", "AXImage"); + testRoleAndSubRole(accDoc, "path", "AXImage"); + testRoleAndSubRole(accDoc, "image", "AXImage"); + } +); + +addAccessibleTask( + ` + <figure id="figure"> + <img id="img" src="http://example.com/a11y/accessible/tests/mochitest/moz.png" alt="Logo"> + <p>Non-image figure content</p> + <figcaption id="figcaption">Old Mozilla logo</figcaption> + </figure>`, + (browser, accDoc) => { + let figure = getNativeInterface(accDoc, "figure"); + ok(!figure.getAttributeValue("AXTitle"), "Figure should not have a title"); + is( + figure.getAttributeValue("AXDescription"), + "Old Mozilla logo", + "Correct figure label" + ); + is(figure.getAttributeValue("AXRole"), "AXGroup", "Correct figure role"); + is( + figure.getAttributeValue("AXRoleDescription"), + "figure", + "Correct figure role description" + ); + + let img = getNativeInterface(accDoc, "img"); + ok(!img.getAttributeValue("AXTitle"), "img should not have a title"); + is(img.getAttributeValue("AXDescription"), "Logo", "Correct img label"); + is(img.getAttributeValue("AXRole"), "AXImage", "Correct img role"); + is( + img.getAttributeValue("AXRoleDescription"), + "image", + "Correct img role description" + ); + + let figcaption = getNativeInterface(accDoc, "figcaption"); + ok( + !figcaption.getAttributeValue("AXTitle"), + "figcaption should not have a title" + ); + ok( + !figcaption.getAttributeValue("AXDescription"), + "figcaption should not have a label" + ); + is( + figcaption.getAttributeValue("AXRole"), + "AXGroup", + "Correct figcaption role" + ); + is( + figcaption.getAttributeValue("AXRoleDescription"), + "group", + "Correct figcaption role description" + ); + } +); + +addAccessibleTask(`<button>hello world</button>`, async (browser, accDoc) => { + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "web area should be an AXWebArea" + ); + ok( + !webArea.attributeNames.includes("AXSubrole"), + "AXWebArea should not have a subrole" + ); + + let roleChanged = waitForMacEvent("AXMozRoleChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.setAttribute("role", "application"); + }); + await roleChanged; + + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "web area should retain AXWebArea role" + ); + ok( + !webArea.attributeNames.includes("AXSubrole"), + "AXWebArea should not have a subrole" + ); + + let rootGroup = webArea.getAttributeValue("AXChildren")[0]; + is(rootGroup.getAttributeValue("AXRole"), "AXGroup"); + is(rootGroup.getAttributeValue("AXSubrole"), "AXLandmarkApplication"); +}); diff --git a/accessible/tests/browser/mac/browser_rootgroup.js b/accessible/tests/browser/mac/browser_rootgroup.js new file mode 100644 index 0000000000..87f5e6c04f --- /dev/null +++ b/accessible/tests/browser/mac/browser_rootgroup.js @@ -0,0 +1,196 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test document with no single group child + */ +addAccessibleTask( + `<p id="p1">hello</p><p>world</p>`, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + is( + rootGroup.getAttributeValue("AXChildren").length, + 2, + "Root group has two children" + ); + + // From bottom-up + let p1 = getNativeInterface(accDoc, "p1"); + rootGroup = p1.getAttributeValue("AXParent"); + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + } +); + +/** + * Test document with a top-level group + */ +addAccessibleTask( + `<div role="grouping" id="group"><p>hello</p><p>world</p></div>`, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + is( + rootGroup.getAttributeValue("AXDOMIdentifier"), + "group", + "Root group is a document element" + ); + + // Adding an 'application' role to the body should + // create a root group with an application subrole. + let evt = waitForMacEvent("AXMozRoleChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.body.setAttribute("role", "application"); + }); + await evt; + + is( + doc.getAttributeValue("AXRole"), + "AXWebArea", + "doc still has web area role" + ); + is( + doc.getAttributeValue("AXRoleDescription"), + "HTML Content", + "doc has correct role description" + ); + ok( + !doc.attributeNames.includes("AXSubrole"), + "sub role not available on web area" + ); + + rootGroup = doc.getAttributeValue("AXChildren")[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + is( + rootGroup.getAttributeValue("AXRole"), + "AXGroup", + "root group has AXGroup role" + ); + is( + rootGroup.getAttributeValue("AXSubrole"), + "AXLandmarkApplication", + "root group has application subrole" + ); + is( + rootGroup.getAttributeValue("AXRoleDescription"), + "application", + "root group has application role description" + ); + } +); + +/** + * Test document with body[role=application] and a top-level group + */ +addAccessibleTask( + `<div role="grouping" id="group"><p>hello</p><p>world</p></div>`, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + is( + doc.getAttributeValue("AXRole"), + "AXWebArea", + "doc still has web area role" + ); + is( + doc.getAttributeValue("AXRoleDescription"), + "HTML Content", + "doc has correct role description" + ); + ok( + !doc.attributeNames.includes("AXSubrole"), + "sub role not available on web area" + ); + + let rootGroup = doc.getAttributeValue("AXChildren")[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + is( + rootGroup.getAttributeValue("AXRole"), + "AXGroup", + "root group has AXGroup role" + ); + is( + rootGroup.getAttributeValue("AXSubrole"), + "AXLandmarkApplication", + "root group has application subrole" + ); + is( + rootGroup.getAttributeValue("AXRoleDescription"), + "application", + "root group has application role description" + ); + }, + { contentDocBodyAttrs: { role: "application" } } +); + +/** + * Test document with a single button + */ +addAccessibleTask( + `<button id="button">I am a button</button>`, + async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + let rootGroupChildren = rootGroup.getAttributeValue("AXChildren"); + is(rootGroupChildren.length, 1, "Root group has one children"); + + is( + rootGroupChildren[0].getAttributeValue("AXRole"), + "AXButton", + "Button is child of root group" + ); + + // From bottom-up + let button = getNativeInterface(accDoc, "button"); + rootGroup = button.getAttributeValue("AXParent"); + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_rotor.js b/accessible/tests/browser/mac/browser_rotor.js new file mode 100644 index 0000000000..8b0d12de9d --- /dev/null +++ b/accessible/tests/browser/mac/browser_rotor.js @@ -0,0 +1,1697 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/states.js */ +loadScripts({ name: "states.js", dir: MOCHITESTS_DIR }); + +ChromeUtils.defineModuleGetter( + this, + "PlacesTestUtils", + "resource://testing-common/PlacesTestUtils.jsm" +); + +/** + * Test rotor with heading + */ +addAccessibleTask( + `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const headingCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, headingCount, "Found two headings"); + + const headings = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + is( + hello.getAttributeValue("AXTitle"), + headings[0].getAttributeValue("AXTitle"), + "Found correct first heading" + ); + is( + world.getAttributeValue("AXTitle"), + headings[1].getAttributeValue("AXTitle"), + "Found correct second heading" + ); + } +); + +/** + * Test rotor with articles + */ +addAccessibleTask( + `<article id="google"> + <h2>Google Chrome</h2> + <p>Google Chrome is a web browser developed by Google, released in 2008. Chrome is the world's most popular web browser today!</p> + </article> + + <article id="moz"> + <h2>Mozilla Firefox</h2> + <p>Mozilla Firefox is an open-source web browser developed by Mozilla. Firefox has been the second most popular web browser since January, 2018.</p> + </article> + + <article id="microsoft"> + <h2>Microsoft Edge</h2> + <p>Microsoft Edge is a web browser developed by Microsoft, released in 2015. Microsoft Edge replaced Internet Explorer.</p> + </article> `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXArticleSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const articleCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(3, articleCount, "Found three articles"); + + const articles = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const google = getNativeInterface(accDoc, "google"); + const moz = getNativeInterface(accDoc, "moz"); + const microsoft = getNativeInterface(accDoc, "microsoft"); + + is( + google.getAttributeValue("AXTitle"), + articles[0].getAttributeValue("AXTitle"), + "Found correct first article" + ); + is( + moz.getAttributeValue("AXTitle"), + articles[1].getAttributeValue("AXTitle"), + "Found correct second article" + ); + is( + microsoft.getAttributeValue("AXTitle"), + articles[2].getAttributeValue("AXTitle"), + "Found correct third article" + ); + } +); + +/** + * Test rotor with tables + */ +addAccessibleTask( + ` + <table id="shapes"> + <tr> + <th>Shape</th> + <th>Color</th> + <th>Do I like it?</th> + </tr> + <tr> + <td>Triangle</td> + <td>Green</td> + <td>No</td> + </tr> + <tr> + <td>Square</td> + <td>Red</td> + <td>Yes</td> + </tr> + </table> + <br> + <table id="food"> + <tr> + <th>Grocery Item</th> + <th>Quantity</th> + </tr> + <tr> + <td>Onions</td> + <td>2</td> + </tr> + <tr> + <td>Yogurt</td> + <td>1</td> + </tr> + <tr> + <td>Spinach</td> + <td>1</td> + </tr> + <tr> + <td>Cherries</td> + <td>12</td> + </tr> + <tr> + <td>Carrots</td> + <td>5</td> + </tr> + </table> + <br> + <div role="table" id="ariaTable"> + <div role="row"> + <div role="cell"> + I am a tiny aria table + </div> + </div> + </div> + <br> + <table role="grid" id="grid"> + <tr> + <th>A</th> + <th>B</th> + <th>C</th> + <th>D</th> + <th>E</th> + </tr> + <tr> + <th>F</th> + <th>G</th> + <th>H</th> + <th>I</th> + <th>J</th> + </tr> + </table> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXTableSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const tableCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, tableCount, "Found four tables"); + + const tables = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const shapes = getNativeInterface(accDoc, "shapes"); + const food = getNativeInterface(accDoc, "food"); + const ariaTable = getNativeInterface(accDoc, "ariaTable"); + const grid = getNativeInterface(accDoc, "grid"); + + is( + shapes.getAttributeValue("AXColumnCount"), + tables[0].getAttributeValue("AXColumnCount"), + "Found correct first table" + ); + is( + food.getAttributeValue("AXColumnCount"), + tables[1].getAttributeValue("AXColumnCount"), + "Found correct second table" + ); + is( + ariaTable.getAttributeValue("AXColumnCount"), + tables[2].getAttributeValue("AXColumnCount"), + "Found correct third table" + ); + is( + grid.getAttributeValue("AXColumnCount"), + tables[3].getAttributeValue("AXColumnCount"), + "Found correct fourth table" + ); + } +); + +/** + * Test rotor with landmarks + */ +addAccessibleTask( + ` + <header id="header"> + <h1>This is a heading within a header</h1> + </header> + + <nav id="nav"> + <a href="example.com">I am a link in a nav</a> + </nav> + + <main id="main"> + I am some text in a main element + </main> + + <footer id="footer"> + <h2>Heading in footer</h2> + </footer> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXLandmarkSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const landmarkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, landmarkCount, "Found four landmarks"); + + const landmarks = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const header = getNativeInterface(accDoc, "header"); + const nav = getNativeInterface(accDoc, "nav"); + const main = getNativeInterface(accDoc, "main"); + const footer = getNativeInterface(accDoc, "footer"); + + is( + header.getAttributeValue("AXSubrole"), + landmarks[0].getAttributeValue("AXSubrole"), + "Found correct first landmark" + ); + is( + nav.getAttributeValue("AXSubrole"), + landmarks[1].getAttributeValue("AXSubrole"), + "Found correct second landmark" + ); + is( + main.getAttributeValue("AXSubrole"), + landmarks[2].getAttributeValue("AXSubrole"), + "Found correct third landmark" + ); + is( + footer.getAttributeValue("AXSubrole"), + landmarks[3].getAttributeValue("AXSubrole"), + "Found correct fourth landmark" + ); + } +); + +/** + * Test rotor with aria landmarks + */ +addAccessibleTask( + ` + <div id="banner" role="banner"> + <h1>This is a heading within a banner</h1> + </div> + + <div id="nav" role="navigation"> + <a href="example.com">I am a link in a nav</a> + </div> + + <div id="main" role="main"> + I am some text in a main element + </div> + + <div id="contentinfo" role="contentinfo"> + <h2>Heading in contentinfo</h2> + </div> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXLandmarkSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const landmarkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, landmarkCount, "Found four landmarks"); + + const landmarks = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const banner = getNativeInterface(accDoc, "banner"); + const nav = getNativeInterface(accDoc, "nav"); + const main = getNativeInterface(accDoc, "main"); + const contentinfo = getNativeInterface(accDoc, "contentinfo"); + + is( + banner.getAttributeValue("AXSubrole"), + landmarks[0].getAttributeValue("AXSubrole"), + "Found correct first landmark" + ); + is( + nav.getAttributeValue("AXSubrole"), + landmarks[1].getAttributeValue("AXSubrole"), + "Found correct second landmark" + ); + is( + main.getAttributeValue("AXSubrole"), + landmarks[2].getAttributeValue("AXSubrole"), + "Found correct third landmark" + ); + is( + contentinfo.getAttributeValue("AXSubrole"), + landmarks[3].getAttributeValue("AXSubrole"), + "Found correct fourth landmark" + ); + } +); + +/** + * Test rotor with buttons + */ +addAccessibleTask( + ` + <button id="button">hello world</button><br> + + <input type="button" value="another kinda button" id="input"><br> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXButtonSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const buttonCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, buttonCount, "Found two buttons"); + + const buttons = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const button = getNativeInterface(accDoc, "button"); + const input = getNativeInterface(accDoc, "input"); + + is( + button.getAttributeValue("AXRole"), + buttons[0].getAttributeValue("AXRole"), + "Found correct button" + ); + is( + input.getAttributeValue("AXRole"), + buttons[1].getAttributeValue("AXRole"), + "Found correct input button" + ); + } +); + +/** + * Test rotor with heading + */ +addAccessibleTask( + `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXHeadingSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const headingCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, headingCount, "Found two headings"); + + const headings = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const hello = getNativeInterface(accDoc, "hello"); + const world = getNativeInterface(accDoc, "world"); + is( + hello.getAttributeValue("AXTitle"), + headings[0].getAttributeValue("AXTitle"), + "Found correct first heading" + ); + is( + world.getAttributeValue("AXTitle"), + headings[1].getAttributeValue("AXTitle"), + "Found correct second heading" + ); + } +); + +/** + * Test rotor with buttons + */ +addAccessibleTask( + ` + <form> + <h2>input[type=button]</h2> + <input type="button" value="apply" id="button1"> + + <h2>input[type=submit]</h2> + <input type="submit" value="submit now" id="submit"> + + <h2>input[type=image]</h2> + <input type="image" src="sample.jpg" alt="submit image" id="image"> + + <h2>input[type=reset]</h2> + <input type="reset" value="reset now" id="reset"> + + <h2>button element</h2> + <button id="button2">Submit button</button> + </form> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(5, controlsCount, "Found 5 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const button1 = getNativeInterface(accDoc, "button1"); + const submit = getNativeInterface(accDoc, "submit"); + const image = getNativeInterface(accDoc, "image"); + const reset = getNativeInterface(accDoc, "reset"); + const button2 = getNativeInterface(accDoc, "button2"); + + is( + button1.getAttributeValue("AXTitle"), + controls[0].getAttributeValue("AXTitle"), + "Found correct first control" + ); + is( + submit.getAttributeValue("AXTitle"), + controls[1].getAttributeValue("AXTitle"), + "Found correct second control" + ); + is( + image.getAttributeValue("AXTitle"), + controls[2].getAttributeValue("AXTitle"), + "Found correct third control" + ); + is( + reset.getAttributeValue("AXTitle"), + controls[3].getAttributeValue("AXTitle"), + "Found correct third control" + ); + is( + button2.getAttributeValue("AXTitle"), + controls[4].getAttributeValue("AXTitle"), + "Found correct third control" + ); + } +); + +/** + * Test rotor with inputs + */ +addAccessibleTask( + ` + <input type="text" value="I'm a text field." id="text"><br> + <input type="text" value="me too" id="implText"><br> + <textarea id="textarea">this is some text in a text area</textarea><br> + <input type="tel" value="0000000000" id="tel"><br> + <input type="url" value="https://example.com" id="url"><br> + <input type="email" value="hi@example.com" id="email"><br> + <input type="password" value="blah" id="password"><br> + <input type="month" value="2020-01" id="month"><br> + <input type="week" value="2020-W01" id="week"><br> + <input type="number" value="12" id="number"><br> + <input type="range" value="12" min="0" max="20" id="range"><br> + <input type="date" value="2020-01-01" id="date"><br> + <input type="time" value="10:10:10" id="time"><br> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(13, controlsCount, "Found 13 controls"); + // the extra controls here come from our time control + // we can't filter out its internal buttons/incrementors + // like we do with the date entry because the time entry + // doesn't have its own specific role -- its just a grouping. + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const text = getNativeInterface(accDoc, "text"); + const implText = getNativeInterface(accDoc, "implText"); + const textarea = getNativeInterface(accDoc, "textarea"); + const tel = getNativeInterface(accDoc, "tel"); + const url = getNativeInterface(accDoc, "url"); + const email = getNativeInterface(accDoc, "email"); + const password = getNativeInterface(accDoc, "password"); + const month = getNativeInterface(accDoc, "month"); + const week = getNativeInterface(accDoc, "week"); + const number = getNativeInterface(accDoc, "number"); + const range = getNativeInterface(accDoc, "range"); + + const toCheck = [ + text, + implText, + textarea, + tel, + url, + email, + password, + month, + week, + number, + range, + ]; + + for (let i = 0; i < toCheck.length; i++) { + is( + toCheck[i].getAttributeValue("AXValue"), + controls[i].getAttributeValue("AXValue"), + "Found correct input control" + ); + } + + const date = getNativeInterface(accDoc, "date"); + const time = getNativeInterface(accDoc, "time"); + + is( + date.getAttributeValue("AXRole"), + controls[11].getAttributeValue("AXRole"), + "Found corrent date editor" + ); + + is( + time.getAttributeValue("AXRole"), + controls[12].getAttributeValue("AXRole"), + "Found corrent time editor" + ); + } +); + +/** + * Test rotor with groupings + */ +addAccessibleTask( + ` + <fieldset> + <legend>Radios</legend> + <div role="radiogroup" id="radios"> + <input id="radio1" type="radio" name="g1" checked="checked"> Radio 1 + <input id="radio2" type="radio" name="g1"> Radio 2 + </div> + </fieldset> + + <fieldset id="checkboxes"> + <legend>Checkboxes</legend> + <input id="checkbox1" type="checkbox" name="g2"> Checkbox 1 + <input id="checkbox2" type="checkbox" name="g2" checked="checked">Checkbox 2 + </fieldset> + + <fieldset id="switches"> + <legend>Switches</legend> + <input id="switch1" name="g3" role="switch" type="checkbox">Switch 1 + <input checked="checked" id="switch2" name="g3" role="switch" type="checkbox">Switch 2 + </fieldset> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(9, controlsCount, "Found 9 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const radios = getNativeInterface(accDoc, "radios"); + const radio1 = getNativeInterface(accDoc, "radio1"); + const radio2 = getNativeInterface(accDoc, "radio2"); + + is( + radios.getAttributeValue("AXRole"), + controls[0].getAttributeValue("AXRole"), + "Found correct group of radios" + ); + is( + radio1.getAttributeValue("AXRole"), + controls[1].getAttributeValue("AXRole"), + "Found correct radio 1" + ); + is( + radio2.getAttributeValue("AXRole"), + controls[2].getAttributeValue("AXRole"), + "Found correct radio 2" + ); + + const checkboxes = getNativeInterface(accDoc, "checkboxes"); + const checkbox1 = getNativeInterface(accDoc, "checkbox1"); + const checkbox2 = getNativeInterface(accDoc, "checkbox2"); + + is( + checkboxes.getAttributeValue("AXRole"), + controls[3].getAttributeValue("AXRole"), + "Found correct group of checkboxes" + ); + is( + checkbox1.getAttributeValue("AXRole"), + controls[4].getAttributeValue("AXRole"), + "Found correct checkbox 1" + ); + is( + checkbox2.getAttributeValue("AXRole"), + controls[5].getAttributeValue("AXRole"), + "Found correct checkbox 2" + ); + + const switches = getNativeInterface(accDoc, "switches"); + const switch1 = getNativeInterface(accDoc, "switch1"); + const switch2 = getNativeInterface(accDoc, "switch2"); + + is( + switches.getAttributeValue("AXRole"), + controls[6].getAttributeValue("AXRole"), + "Found correct group of switches" + ); + is( + switch1.getAttributeValue("AXRole"), + controls[7].getAttributeValue("AXRole"), + "Found correct switch 1" + ); + is( + switch2.getAttributeValue("AXRole"), + controls[8].getAttributeValue("AXRole"), + "Found correct switch 2" + ); + } +); + +/** + * Test rotor with misc controls + */ +addAccessibleTask( + ` + <input role="spinbutton" id="spinbutton" type="number" value="25"> + + <details id="details"> + <summary>Hello</summary> + world + </details> + + <ul role="tree" id="tree"> + <li role="treeitem">item1</li> + <li role="treeitem">item1</li> + </ul> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXControlSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const controlsCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(3, controlsCount, "Found 3 controls"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const spin = getNativeInterface(accDoc, "spinbutton"); + const details = getNativeInterface(accDoc, "details"); + const tree = getNativeInterface(accDoc, "tree"); + + is( + spin.getAttributeValue("AXRole"), + controls[0].getAttributeValue("AXRole"), + "Found correct spinbutton" + ); + is( + details.getAttributeValue("AXRole"), + controls[1].getAttributeValue("AXRole"), + "Found correct details element" + ); + is( + tree.getAttributeValue("AXRole"), + controls[2].getAttributeValue("AXRole"), + "Found correct tree" + ); + } +); + +/** + * Test rotor with links + */ +addAccessibleTask( + ` + <a href="" id="empty">empty link</a> + <a href="http://www.example.com/" id="href">Example link</a> + <a id="noHref">link without href</a> + `, + async (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXLinkSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let linkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(2, linkCount, "Found two links"); + + let links = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + const empty = getNativeInterface(accDoc, "empty"); + const href = getNativeInterface(accDoc, "href"); + + is( + empty.getAttributeValue("AXTitle"), + links[0].getAttributeValue("AXTitle"), + "Found correct first link" + ); + is( + href.getAttributeValue("AXTitle"), + links[1].getAttributeValue("AXTitle"), + "Found correct second link" + ); + + // unvisited links + + searchPred = { + AXSearchKey: "AXUnvisitedLinkSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + linkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, linkCount, "Found two links"); + + links = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + empty.getAttributeValue("AXTitle"), + links[0].getAttributeValue("AXTitle"), + "Found correct first link" + ); + is( + href.getAttributeValue("AXTitle"), + links[1].getAttributeValue("AXTitle"), + "Found correct second link" + ); + + // visited links + + let stateChanged = waitForEvent(EVENT_STATE_CHANGE, "href"); + + await PlacesTestUtils.addVisits(["http://www.example.com/"]); + + await stateChanged; + + searchPred = { + AXSearchKey: "AXVisitedLinkSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + linkCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(1, linkCount, "Found one link"); + + links = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + href.getAttributeValue("AXTitle"), + links[0].getAttributeValue("AXTitle"), + "Found correct visited link" + ); + + // Ensure history is cleared before running again + await PlacesUtils.history.clear(); + } +); + +/* + * Test AXAnyTypeSearchKey with root group + */ +addAccessibleTask( + `<h1 id="hello">hello</h1><br><h2 id="world">world</h2><br>goodbye`, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: 1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + is(results.length, 1, "One result for root group"); + is( + results[0].getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + searchPred.AXStartElement = results[0]; + results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + is(results.length, 0, "No more results past root group"); + + searchPred.AXDirection = "AXDirectionPrevious"; + results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + is( + results.length, + 0, + "Searching backwards from root group should yield no results" + ); + + const rootGroup = webArea.getAttributeValue("AXChildren")[0]; + is( + rootGroup.getAttributeValue("AXIdentifier"), + "root-group", + "Is generated root group" + ); + + searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: 1, + AXDirection: "AXDirectionNext", + }; + + results = rootGroup.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + results[0].getAttributeValue("AXRole"), + "AXHeading", + "Is first heading child" + ); + } +); + +/** + * Test rotor with checkboxes + */ +addAccessibleTask( + ` + <fieldset id="checkboxes"> + <legend>Checkboxes</legend> + <input id="checkbox1" type="checkbox" name="g2"> Checkbox 1 + <input id="checkbox2" type="checkbox" name="g2" checked="checked">Checkbox 2 + <div id="checkbox3" role="checkbox">Checkbox 3</div> + <div id="checkbox4" role="checkbox" aria-checked="true">Checkbox 4</div> + </fieldset> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXCheckboxSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const checkboxCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(4, checkboxCount, "Found 4 checkboxes"); + + const checkboxes = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const checkbox1 = getNativeInterface(accDoc, "checkbox1"); + const checkbox2 = getNativeInterface(accDoc, "checkbox2"); + const checkbox3 = getNativeInterface(accDoc, "checkbox3"); + const checkbox4 = getNativeInterface(accDoc, "checkbox4"); + + is( + checkbox1.getAttributeValue("AXValue"), + checkboxes[0].getAttributeValue("AXValue"), + "Found correct checkbox 1" + ); + is( + checkbox2.getAttributeValue("AXValue"), + checkboxes[1].getAttributeValue("AXValue"), + "Found correct checkbox 2" + ); + is( + checkbox3.getAttributeValue("AXValue"), + checkboxes[2].getAttributeValue("AXValue"), + "Found correct checkbox 3" + ); + is( + checkbox4.getAttributeValue("AXValue"), + checkboxes[3].getAttributeValue("AXValue"), + "Found correct checkbox 4" + ); + } +); + +/** + * Test rotor with radiogroups + */ +addAccessibleTask( + ` + <div role="radiogroup" id="radios" aria-labelledby="desc"> + <h1 id="desc">some radio buttons</h1> + <div id="radio1" role="radio"> Radio 1</div> + <div id="radio2" role="radio"> Radio 2</div> + </div> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXRadioGroupSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const radiogroupCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(1, radiogroupCount, "Found 1 radio group"); + + const controls = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const radios = getNativeInterface(accDoc, "radios"); + + is( + radios.getAttributeValue("AXDescription"), + controls[0].getAttributeValue("AXDescription"), + "Found correct group of radios" + ); + } +); + +/* + * Test rotor with inputs + */ +addAccessibleTask( + ` + <input type="text" value="I'm a text field." id="text"><br> + <input type="text" value="me too" id="implText"><br> + <textarea id="textarea">this is some text in a text area</textarea><br> + <input type="tel" value="0000000000" id="tel"><br> + <input type="url" value="https://example.com" id="url"><br> + <input type="email" value="hi@example.com" id="email"><br> + <input type="password" value="blah" id="password"><br> + <input type="month" value="2020-01" id="month"><br> + <input type="week" value="2020-W01" id="week"><br> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXTextFieldSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textfieldCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(9, textfieldCount, "Found 9 fields"); + + const fields = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const text = getNativeInterface(accDoc, "text"); + const implText = getNativeInterface(accDoc, "implText"); + const textarea = getNativeInterface(accDoc, "textarea"); + const tel = getNativeInterface(accDoc, "tel"); + const url = getNativeInterface(accDoc, "url"); + const email = getNativeInterface(accDoc, "email"); + const password = getNativeInterface(accDoc, "password"); + const month = getNativeInterface(accDoc, "month"); + const week = getNativeInterface(accDoc, "week"); + + const toCheck = [ + text, + implText, + textarea, + tel, + url, + email, + password, + month, + week, + ]; + + for (let i = 0; i < toCheck.length; i++) { + is( + toCheck[i].getAttributeValue("AXValue"), + fields[i].getAttributeValue("AXValue"), + "Found correct input control" + ); + } + } +); + +/** + * Test rotor with static text + */ +addAccessibleTask( + ` + <h1>Hello I am a heading</h1> + This is some regular text.<p>this is some paragraph text</p><br> + This is a list:<ul> + <li>List item one</li> + <li>List item two</li> + </ul> + + <a href="http://example.com">This is a link</a> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXStaticTextSearchKey", + AXImmediateDescendants: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(7, textCount, "Found 7 pieces of text"); + + const text = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + "Hello I am a heading", + text[0].getAttributeValue("AXValue"), + "Found correct text node for heading" + ); + is( + "This is some regular text.", + text[1].getAttributeValue("AXValue"), + "Found correct text node" + ); + is( + "this is some paragraph text", + text[2].getAttributeValue("AXValue"), + "Found correct text node for paragraph" + ); + is( + "This is a list:", + text[3].getAttributeValue("AXValue"), + "Found correct text node for pre-list text node" + ); + is( + "List item one", + text[4].getAttributeValue("AXValue"), + "Found correct text node for list item one" + ); + is( + "List item two", + text[5].getAttributeValue("AXValue"), + "Found correct text node for list item two" + ); + is( + "This is a link", + text[6].getAttributeValue("AXValue"), + "Found correct text node for link" + ); + } +); + +/** + * Test rotor with lists + */ +addAccessibleTask( + ` + <ul id="unordered"> + <li>hello</li> + <li>world</li> + </ul> + + <ol id="ordered"> + <li>item one</li> + <li>item two</li> + </ol> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXListSearchKey", + AXImmediateDescendants: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const listCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + + is(2, listCount, "Found 2 lists"); + + const lists = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + const ordered = getNativeInterface(accDoc, "ordered"); + const unordered = getNativeInterface(accDoc, "unordered"); + + is( + unordered.getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"), + lists[0].getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"), + "Found correct unordered list" + ); + is( + ordered.getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"), + lists[1].getAttributeValue("AXChildren")[0].getAttributeValue("AXTitle"), + "Found correct ordered list" + ); + } +); + +/* + * Test rotor with images + */ +addAccessibleTask( + ` + <img id="img1" alt="image one" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"><br> + <a href="http://example.com"> + <img id="img2" alt="image two" src="http://example.com/a11y/accessible/tests/mochitest/moz.png"> + </a> + <img src="" id="img3"> + `, + (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXImageSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + let images = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is(images.length, 3, "Found three images"); + + const img1 = getNativeInterface(accDoc, "img1"); + const img2 = getNativeInterface(accDoc, "img2"); + const img3 = getNativeInterface(accDoc, "img3"); + + is( + img1.getAttributeValue("AXDescription"), + images[0].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img2.getAttributeValue("AXDescription"), + images[1].getAttributeValue("AXDescription"), + "Found correct image" + ); + + is( + img3.getAttributeValue("AXDescription"), + images[2].getAttributeValue("AXDescription"), + "Found correct image" + ); + } +); + +/** + * Test rotor with frames + */ +addAccessibleTask( + ` + <iframe id="frame1" src="data:text/html,<h1>hello</h1>world"></iframe> + <iframe id="frame2" src="data:text/html,<iframe id='frame3' src='data:text/html,<h1>goodbye</h1>'>"></iframe> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXFrameSearchKey", + AXImmediateDescendantsOnly: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const frameCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(3, frameCount, "Found 3 frames"); + } +); + +/** + * Test rotor with static text + */ +addAccessibleTask( + ` + <h1>Hello I am a heading</h1> + This is some regular text.<p>this is some paragraph text</p><br> + This is a list:<ul> + <li>List item one</li> + <li>List item two</li> + </ul> + + <a href="http://example.com">This is a link</a> + `, + async (browser, accDoc) => { + const searchPred = { + AXSearchKey: "AXStaticTextSearchKey", + AXImmediateDescendants: 0, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(7, textCount, "Found 7 pieces of text"); + + const text = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + is( + "Hello I am a heading", + text[0].getAttributeValue("AXValue"), + "Found correct text node for heading" + ); + is( + "This is some regular text.", + text[1].getAttributeValue("AXValue"), + "Found correct text node" + ); + is( + "this is some paragraph text", + text[2].getAttributeValue("AXValue"), + "Found correct text node for paragraph" + ); + is( + "This is a list:", + text[3].getAttributeValue("AXValue"), + "Found correct text node for pre-list text node" + ); + is( + "List item one", + text[4].getAttributeValue("AXValue"), + "Found correct text node for list item one" + ); + is( + "List item two", + text[5].getAttributeValue("AXValue"), + "Found correct text node for list item two" + ); + is( + "This is a link", + text[6].getAttributeValue("AXValue"), + "Found correct text node for link" + ); + } +); + +/** + * Test search with non-webarea root + */ +addAccessibleTask( + ` + <div id="searchroot"><p id="p1">hello</p><p id="p2">world</p></div> + <div><p>goodybe</p></div> + `, + async (browser, accDoc) => { + let searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + }; + + const searchRoot = getNativeInterface(accDoc, "searchroot"); + const resultCount = searchRoot.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(resultCount, 2, "Found 2 items"); + + const p1 = getNativeInterface(accDoc, "p1"); + searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + AXStartElement: p1, + }; + + let results = searchRoot.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + Assert.deepEqual( + results.map(r => r.getAttributeValue("AXDOMIdentifier")), + ["p2"], + "Result is next group sibling" + ); + + searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXImmediateDescendantsOnly: 1, + AXResultsLimit: -1, + AXDirection: "AXDirectionPrevious", + }; + + results = searchRoot.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + Assert.deepEqual( + results.map(r => r.getAttributeValue("AXDOMIdentifier")), + ["p2", "p1"], + "A reverse search should return groups in reverse" + ); + } +); + +/** + * Test search text + */ +addAccessibleTask( + ` + <p>It's about the future, isn't it?</p> + <p>Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.</p> + <ul> + <li>I could hang out, you could show me around.</li> + <li>There's that word again, heavy.</li> + </ul> + `, + async (browser, f, accDoc) => { + let searchPred = { + AXSearchKey: "AXAnyTypeSearchKey", + AXResultsLimit: -1, + AXDirection: "AXDirectionNext", + AXSearchText: "could", + }; + + const webArea = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + is( + webArea.getAttributeValue("AXRole"), + "AXWebArea", + "Got web area accessible" + ); + + const textSearchCount = webArea.getParameterizedAttributeValue( + "AXUIElementCountForSearchPredicate", + NSDictionary(searchPred) + ); + is(textSearchCount, 2, "Found 2 matching items in text search"); + + const results = webArea.getParameterizedAttributeValue( + "AXUIElementsForSearchPredicate", + NSDictionary(searchPred) + ); + + info(results.map(r => r.getAttributeValue("AXMozDebugDescription"))); + + Assert.deepEqual( + results.map(r => r.getAttributeValue("AXValue")), + [ + "Okay, alright, Saturday is good, Saturday's good, I could spend a week in 1955.", + "I could hang out, you could show me around.", + ], + "Correct text search results" + ); + }, + { topLevel: false, iframe: true, remoteIframe: true } +); diff --git a/accessible/tests/browser/mac/browser_selectables.js b/accessible/tests/browser/mac/browser_selectables.js new file mode 100644 index 0000000000..29cd326bdd --- /dev/null +++ b/accessible/tests/browser/mac/browser_selectables.js @@ -0,0 +1,336 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function getSelectedIds(selectable) { + return selectable + .getAttributeValue("AXSelectedChildren") + .map(c => c.getAttributeValue("AXDOMIdentifier")); +} + +/** + * Test aria tabs + */ +addAccessibleTask("mac/doc_aria_tabs.html", async (browser, accDoc) => { + let tablist = getNativeInterface(accDoc, "tablist"); + is( + tablist.getAttributeValue("AXRole"), + "AXTabGroup", + "Correct role for tablist" + ); + + let tabMacAccs = tablist.getAttributeValue("AXTabs"); + is(tabMacAccs.length, 3, "3 items in AXTabs"); + + let selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + + let tab = selectedTabs[0]; + is(tab.getAttributeValue("AXRole"), "AXRadioButton", "Correct role for tab"); + is( + tab.getAttributeValue("AXSubrole"), + "AXTabButton", + "Correct subrole for tab" + ); + is(tab.getAttributeValue("AXTitle"), "First Tab", "Correct title for tab"); + + let tabToSelect = tabMacAccs[1]; + is( + tabToSelect.getAttributeValue("AXTitle"), + "Second Tab", + "Correct title for tab" + ); + + let actions = tabToSelect.actionNames; + ok(true, actions); + ok(actions.includes("AXPress"), "Has switch action"); + + let evt = waitForMacEvent("AXSelectedChildrenChanged"); + tabToSelect.performAction("AXPress"); + await evt; + + selectedTabs = tablist.getAttributeValue("AXSelectedChildren"); + is(selectedTabs.length, 1, "one selected tab"); + is( + selectedTabs[0].getAttributeValue("AXTitle"), + "Second Tab", + "Correct title for tab" + ); +}); + +addAccessibleTask('<p id="p">hello</p>', async (browser, accDoc) => { + let p = getNativeInterface(accDoc, "p"); + ok( + p.attributeNames.includes("AXSelected"), + "html element includes 'AXSelected' attribute" + ); + is(p.getAttributeValue("AXSelected"), 0, "AX selected is 'false'"); +}); + +addAccessibleTask( + `<select id="select" aria-label="Choose a number" multiple> + <option id="one" selected>One</option> + <option id="two">Two</option> + <option id="three">Three</option> + <option id="four" disabled>Four</option> + </select>`, + async (browser, accDoc) => { + let select = getNativeInterface(accDoc, "select"); + let one = getNativeInterface(accDoc, "one"); + let two = getNativeInterface(accDoc, "two"); + let three = getNativeInterface(accDoc, "three"); + let four = getNativeInterface(accDoc, "four"); + + is( + select.getAttributeValue("AXTitle"), + "Choose a number", + "Select titled correctly" + ); + ok( + select.attributeNames.includes("AXOrientation"), + "Have orientation attribute" + ); + ok( + select.isAttributeSettable("AXSelectedChildren"), + "Select can have AXSelectedChildren set" + ); + + is(one.getAttributeValue("AXTitle"), "", "Option should not have a title"); + is( + one.getAttributeValue("AXValue"), + "One", + "Option should have correct value" + ); + is( + one.getAttributeValue("AXRole"), + "AXStaticText", + "Options should have AXStaticText role" + ); + ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set"); + + is(select.getAttributeValue("AXSelectedChildren").length, 1); + one.setAttributeValue("AXSelected", false); + is(select.getAttributeValue("AXSelectedChildren").length, 0); + + three.setAttributeValue("AXSelected", true); + is(select.getAttributeValue("AXSelectedChildren").length, 1); + ok(getSelectedIds(select).includes("three"), "'three' is selected"); + + select.setAttributeValue("AXSelectedChildren", [one, two]); + Assert.deepEqual( + getSelectedIds(select), + ["one", "two"], + "one and two are selected" + ); + + select.setAttributeValue("AXSelectedChildren", [three, two, four]); + Assert.deepEqual( + getSelectedIds(select), + ["two", "three"], + "two and three are selected, four is disabled so it's not" + ); + + ok(!four.getAttributeValue("AXEnabled"), "Disabled option is disabled"); + } +); + +addAccessibleTask( + `<select id="select" aria-label="Choose a thing" multiple> + <optgroup label="Fruits"> + <option id="banana" selected>Banana</option> + <option id="apple">Apple</option> + <option id="orange">Orange</option> + </optgroup> + <optgroup label="Vegetables"> + <option id="lettuce" selected>Lettuce</option> + <option id="tomato">Tomato</option> + <option id="onion">Onion</option> + </optgroup> + <optgroup label="Spices"> + <option id="cumin">Cumin</option> + <option id="coriander">Coriander</option> + <option id="allspice" selected>Allspice</option> + </optgroup> + <option id="everything">Everything</option> + </select>`, + async (browser, accDoc) => { + let select = getNativeInterface(accDoc, "select"); + + is( + select.getAttributeValue("AXTitle"), + "Choose a thing", + "Select titled correctly" + ); + ok( + select.attributeNames.includes("AXOrientation"), + "Have orientation attribute" + ); + ok( + select.isAttributeSettable("AXSelectedChildren"), + "Select can have AXSelectedChildren set" + ); + let childValueSelectablePairs = select + .getAttributeValue("AXChildren") + .map(c => [ + c.getAttributeValue("AXValue"), + c.isAttributeSettable("AXSelected"), + c.getAttributeValue("AXEnabled"), + ]); + Assert.deepEqual( + childValueSelectablePairs, + [ + ["Fruits", false, false], + ["Banana", true, true], + ["Apple", true, true], + ["Orange", true, true], + ["Vegetables", false, false], + ["Lettuce", true, true], + ["Tomato", true, true], + ["Onion", true, true], + ["Spices", false, false], + ["Cumin", true, true], + ["Coriander", true, true], + ["Allspice", true, true], + ["Everything", true, true], + ], + "Options are selectable, group labels are not" + ); + + let allspice = getNativeInterface(accDoc, "allspice"); + is( + allspice.getAttributeValue("AXTitle"), + "", + "Option should not have a title" + ); + is( + allspice.getAttributeValue("AXValue"), + "Allspice", + "Option should have a value" + ); + is( + allspice.getAttributeValue("AXRole"), + "AXStaticText", + "Options should have AXStaticText role" + ); + ok( + allspice.isAttributeSettable("AXSelected"), + "Option can have AXSelected set" + ); + is( + allspice + .getAttributeValue("AXParent") + .getAttributeValue("AXDOMIdentifier"), + "select", + "Select is direct parent of nested option" + ); + + let groupLabel = select.getAttributeValue("AXChildren")[0]; + ok( + !groupLabel.isAttributeSettable("AXSelected"), + "Group label should not be selectable" + ); + is( + groupLabel.getAttributeValue("AXValue"), + "Fruits", + "Group label should have a value" + ); + is( + groupLabel.getAttributeValue("AXTitle"), + null, + "Group label should not have a title" + ); + is( + groupLabel.getAttributeValue("AXRole"), + "AXStaticText", + "Group label should have AXStaticText role" + ); + is( + groupLabel + .getAttributeValue("AXParent") + .getAttributeValue("AXDOMIdentifier"), + "select", + "Select is direct parent of group label" + ); + + Assert.deepEqual(getSelectedIds(select), ["banana", "lettuce", "allspice"]); + } +); + +addAccessibleTask( + `<div role="listbox" id="select" aria-label="Choose a number" aria-multiselectable="true"> + <div role="option" id="one" aria-selected="true">One</div> + <div role="option" id="two">Two</div> + <div role="option" id="three">Three</div> + <div role="option" id="four" aria-disabled="true">Four</div> +</div>`, + async (browser, accDoc) => { + let select = getNativeInterface(accDoc, "select"); + let one = getNativeInterface(accDoc, "one"); + let two = getNativeInterface(accDoc, "two"); + let three = getNativeInterface(accDoc, "three"); + let four = getNativeInterface(accDoc, "four"); + + is( + select.getAttributeValue("AXTitle"), + "Choose a number", + "Select titled correctly" + ); + ok( + select.attributeNames.includes("AXOrientation"), + "Have orientation attribute" + ); + ok( + select.isAttributeSettable("AXSelectedChildren"), + "Select can have AXSelectedChildren set" + ); + + is(one.getAttributeValue("AXTitle"), "", "Option should not have a title"); + is( + one.getAttributeValue("AXValue"), + "One", + "Option should have correct value" + ); + is( + one.getAttributeValue("AXRole"), + "AXStaticText", + "Options should have AXStaticText role" + ); + ok(one.isAttributeSettable("AXSelected"), "Option can have AXSelected set"); + + is(select.getAttributeValue("AXSelectedChildren").length, 1); + let evt = waitForMacEvent("AXSelectedChildrenChanged"); + // Change selection from content. + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("one").removeAttribute("aria-selected"); + }); + await evt; + is(select.getAttributeValue("AXSelectedChildren").length, 0); + + three.setAttributeValue("AXSelected", true); + is(select.getAttributeValue("AXSelectedChildren").length, 1); + ok(getSelectedIds(select).includes("three"), "'three' is selected"); + + select.setAttributeValue("AXSelectedChildren", [one, two]); + Assert.deepEqual( + getSelectedIds(select), + ["one", "two"], + "one and two are selected" + ); + + select.setAttributeValue("AXSelectedChildren", [three, two, four]); + Assert.deepEqual( + getSelectedIds(select), + ["two", "three"], + "two and three are selected, four is disabled so it's not" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_table.js b/accessible/tests/browser/mac/browser_table.js new file mode 100644 index 0000000000..34aeb87a17 --- /dev/null +++ b/accessible/tests/browser/mac/browser_table.js @@ -0,0 +1,281 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +/* import-globals-from ../../mochitest/attributes.js */ +loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); + +/** + * Helper function to test table consistency. + */ +function testTableConsistency(table, expectedRowCount, expectedColumnCount) { + is(table.getAttributeValue("AXRole"), "AXTable", "Correct role for table"); + + let tableChildren = table.getAttributeValue("AXChildren"); + // XXX: Should be expectedRowCount+ExpectedColumnCount+1 children, rows (incl headers) + cols + headers + // if we're trying to match Safari. + is( + tableChildren.length, + expectedRowCount + expectedColumnCount, + "Table has children = rows (4) + cols (3)" + ); + for (let i = 0; i < tableChildren.length; i++) { + let currChild = tableChildren[i]; + if (i < expectedRowCount) { + is( + currChild.getAttributeValue("AXRole"), + "AXRow", + "Correct role for row" + ); + } else { + is( + currChild.getAttributeValue("AXRole"), + "AXColumn", + "Correct role for col" + ); + is( + currChild.getAttributeValue("AXRoleDescription"), + "column", + "Correct role desc for col" + ); + } + } + + is( + table.getAttributeValue("AXColumnCount"), + expectedColumnCount, + "Table has correct column count." + ); + is( + table.getAttributeValue("AXRowCount"), + expectedRowCount, + "Table has correct row count." + ); + + let cols = table.getAttributeValue("AXColumns"); + is(cols.length, expectedColumnCount, "Table has col list of correct length"); + for (let i = 0; i < cols.length; i++) { + let currCol = cols[i]; + let currChildren = currCol.getAttributeValue("AXChildren"); + is( + currChildren.length, + expectedRowCount, + "Column has correct number of cells" + ); + for (let j = 0; j < currChildren.length; j++) { + let currChild = currChildren[j]; + is( + currChild.getAttributeValue("AXRole"), + "AXCell", + "Column child is cell" + ); + } + } + + let rows = table.getAttributeValue("AXRows"); + is(rows.length, expectedRowCount, "Table has row list of correct length"); + for (let i = 0; i < rows.length; i++) { + let currRow = rows[i]; + let currChildren = currRow.getAttributeValue("AXChildren"); + is( + currChildren.length, + expectedColumnCount, + "Row has correct number of cells" + ); + for (let j = 0; j < currChildren.length; j++) { + let currChild = currChildren[j]; + is(currChild.getAttributeValue("AXRole"), "AXCell", "Row child is cell"); + } + } +} + +/** + * Test table, columns, rows + */ +addAccessibleTask( + `<table id="customers"> + <tbody> + <tr id="firstrow"><th>Company</th><th>Contact</th><th>Country</th></tr> + <tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr> + <tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr> + <tr><td>Ernst Handel</td><td>Roland Mendel</td><td>Austria</td></tr> + </tbody> + </table>`, + async (browser, accDoc) => { + let table = getNativeInterface(accDoc, "customers"); + testTableConsistency(table, 4, 3); + + const rowText = [ + "Madrigal Electromotive GmbH", + "Lydia Rodarte-Quayle", + "Germany", + ]; + let reorder = waitForEvent(EVENT_REORDER, "customers"); + await SpecialPowers.spawn(browser, [rowText], _rowText => { + let tr = content.document.createElement("tr"); + for (let t of _rowText) { + let td = content.document.createElement("td"); + td.textContent = t; + tr.appendChild(td); + } + content.document.getElementById("customers").appendChild(tr); + }); + await reorder; + + let cols = table.getAttributeValue("AXColumns"); + is(cols.length, 3, "Table has col list of correct length"); + for (let i = 0; i < cols.length; i++) { + let currCol = cols[i]; + let currChildren = currCol.getAttributeValue("AXChildren"); + is(currChildren.length, 5, "Column has correct number of cells"); + let lastCell = currChildren[currChildren.length - 1]; + let cellChildren = lastCell.getAttributeValue("AXChildren"); + is(cellChildren.length, 1, "Cell has a single text child"); + is( + cellChildren[0].getAttributeValue("AXRole"), + "AXStaticText", + "Correct role for cell child" + ); + is( + cellChildren[0].getAttributeValue("AXValue"), + rowText[i], + "Correct text for cell" + ); + } + + reorder = waitForEvent(EVENT_REORDER, "firstrow"); + await SpecialPowers.spawn(browser, [], () => { + let td = content.document.createElement("td"); + td.textContent = "Ticker"; + content.document.getElementById("firstrow").appendChild(td); + }); + await reorder; + + cols = table.getAttributeValue("AXColumns"); + is(cols.length, 4, "Table has col list of correct length"); + is( + cols[cols.length - 1].getAttributeValue("AXChildren").length, + 1, + "Last column has single child" + ); + } +); + +addAccessibleTask( + `<table id="table"> + <tr> + <th colspan="2" id="header1">Header 1</th> + <th id="header2">Header 2</th> + </tr> + <tr> + <td id="cell1">one</td> + <td id="cell2" rowspan="2">two</td> + <td id="cell3">three</td> + </tr> + <tr> + <td id="cell4">four</td> + <td id="cell5">five</td> + </tr> + </table>`, + (browser, accDoc) => { + let table = getNativeInterface(accDoc, "table"); + + let getCellAt = (col, row) => + table.getParameterizedAttributeValue("AXCellForColumnAndRow", [col, row]); + + function testCell(cell, expectedId, expectedColRange, expectedRowRange) { + is( + cell.getAttributeValue("AXDOMIdentifier"), + expectedId, + "Correct DOM Identifier" + ); + Assert.deepEqual( + cell.getAttributeValue("AXColumnIndexRange"), + expectedColRange, + "Correct column range" + ); + Assert.deepEqual( + cell.getAttributeValue("AXRowIndexRange"), + expectedRowRange, + "Correct row range" + ); + } + + testCell(getCellAt(0, 0), "header1", [0, 2], [0, 1]); + testCell(getCellAt(1, 0), "header1", [0, 2], [0, 1]); + testCell(getCellAt(2, 0), "header2", [2, 1], [0, 1]); + + testCell(getCellAt(0, 1), "cell1", [0, 1], [1, 1]); + testCell(getCellAt(1, 1), "cell2", [1, 1], [1, 2]); + testCell(getCellAt(2, 1), "cell3", [2, 1], [1, 1]); + + testCell(getCellAt(0, 2), "cell4", [0, 1], [2, 1]); + testCell(getCellAt(1, 2), "cell2", [1, 1], [1, 2]); + testCell(getCellAt(2, 2), "cell5", [2, 1], [2, 1]); + + let colHeaders = table.getAttributeValue("AXColumnHeaderUIElements"); + Assert.deepEqual( + colHeaders.map(c => c.getAttributeValue("AXDOMIdentifier")), + ["header1", "header1", "header2"], + "Correct column headers" + ); + } +); + +addAccessibleTask( + `<table id="table"> + <tr> + <td>Foo</td> + </tr> + </table>`, + (browser, accDoc) => { + // Make sure we guess this table to be a layout table. + testAttrs( + findAccessibleChildByID(accDoc, "table"), + { "layout-guess": "true" }, + true + ); + + let table = getNativeInterface(accDoc, "table"); + is( + table.getAttributeValue("AXRole"), + "AXGroup", + "Correct role (AXGroup) for layout table" + ); + + let children = table.getAttributeValue("AXChildren"); + is( + children.length, + 1, + "Layout table has single child (no additional columns)" + ); + } +); + +addAccessibleTask( + `<div id="table" role="table"> + <span style="display: block;"> + <div role="row"> + <div role="cell">Cell 1</div> + <div role="cell">Cell 2</div> + </div> + </span> + <span style="display: block;"> + <div role="row"> + <span style="display: block;"> + <div role="cell">Cell 3</div> + <div role="cell">Cell 4</div> + </span> + </div> + </span> + </div>`, + async (browser, accDoc) => { + let table = getNativeInterface(accDoc, "table"); + testTableConsistency(table, 2, 2); + } +); diff --git a/accessible/tests/browser/mac/browser_text_basics.js b/accessible/tests/browser/mac/browser_text_basics.js new file mode 100644 index 0000000000..de66ebf639 --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_basics.js @@ -0,0 +1,282 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +function testRangeAtMarker(macDoc, marker, attribute, expected, msg) { + let range = macDoc.getParameterizedAttributeValue(attribute, marker); + is(stringForRange(macDoc, range), expected, msg); +} + +function testUIElement( + macDoc, + marker, + msg, + expectedRole, + expectedValue, + expectedRange +) { + let elem = macDoc.getParameterizedAttributeValue( + "AXUIElementForTextMarker", + marker + ); + is( + elem.getAttributeValue("AXRole"), + expectedRole, + `${msg}: element role matches` + ); + is(elem.getAttributeValue("AXValue"), expectedValue, `${msg}: element value`); + let elemRange = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUIElement", + elem + ); + is( + stringForRange(macDoc, elemRange), + expectedRange, + `${msg}: element range matches element value` + ); +} + +function testStyleRun(macDoc, marker, msg, expectedStyleRun) { + testRangeAtMarker( + macDoc, + marker, + "AXStyleTextMarkerRangeForTextMarker", + expectedStyleRun, + `${msg}: style run matches` + ); +} + +function testParagraph(macDoc, marker, msg, expectedParagraph) { + testRangeAtMarker( + macDoc, + marker, + "AXParagraphTextMarkerRangeForTextMarker", + expectedParagraph, + `${msg}: paragraph matches` + ); +} + +function testWords(macDoc, marker, msg, expectedLeft, expectedRight) { + testRangeAtMarker( + macDoc, + marker, + "AXLeftWordTextMarkerRangeForTextMarker", + expectedLeft, + `${msg}: left word matches` + ); + + testRangeAtMarker( + macDoc, + marker, + "AXRightWordTextMarkerRangeForTextMarker", + expectedRight, + `${msg}: right word matches` + ); +} + +function testLines( + macDoc, + marker, + msg, + expectedLine, + expectedLeft, + expectedRight +) { + testRangeAtMarker( + macDoc, + marker, + "AXLineTextMarkerRangeForTextMarker", + expectedLine, + `${msg}: line matches` + ); + + testRangeAtMarker( + macDoc, + marker, + "AXLeftLineTextMarkerRangeForTextMarker", + expectedLeft, + `${msg}: left line matches` + ); + + testRangeAtMarker( + macDoc, + marker, + "AXRightLineTextMarkerRangeForTextMarker", + expectedRight, + `${msg}: right line matches` + ); +} + +// Tests consistency in text markers between: +// 1. "Linked list" forward navagation +// 2. Getting markers by index +// 3. "Linked list" reverse navagation +// For each iteration method check that the returned index is consistent +function testMarkerIntegrity(accDoc, expectedMarkerValues) { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let count = 0; + + // Iterate forward with "AXNextTextMarkerForTextMarker" + let marker = macDoc.getAttributeValue("AXStartTextMarker"); + while (marker) { + let index = macDoc.getParameterizedAttributeValue( + "AXIndexForTextMarker", + marker + ); + is( + index, + count, + `Correct index in "AXNextTextMarkerForTextMarker": ${count}` + ); + + testWords( + macDoc, + marker, + `At index ${count}`, + ...expectedMarkerValues[count].words + ); + testLines( + macDoc, + marker, + `At index ${count}`, + ...expectedMarkerValues[count].lines + ); + testUIElement( + macDoc, + marker, + `At index ${count}`, + ...expectedMarkerValues[count].element + ); + testParagraph( + macDoc, + marker, + `At index ${count}`, + expectedMarkerValues[count].paragraph + ); + testStyleRun( + macDoc, + marker, + `At index ${count}`, + expectedMarkerValues[count].style + ); + + let prevMarker = marker; + marker = macDoc.getParameterizedAttributeValue( + "AXNextTextMarkerForTextMarker", + marker + ); + + if (marker) { + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [prevMarker, marker] + ); + is( + macDoc.getParameterizedAttributeValue( + "AXLengthForTextMarkerRange", + range + ), + 1, + "marker moved one character" + ); + } + + count++; + } + + // Use "AXTextMarkerForIndex" to retrieve all text markers + for (let i = 0; i < count; i++) { + marker = macDoc.getParameterizedAttributeValue("AXTextMarkerForIndex", i); + let index = macDoc.getParameterizedAttributeValue( + "AXIndexForTextMarker", + marker + ); + is(index, i, `Correct index in "AXTextMarkerForIndex": ${i}`); + } + + ok( + !macDoc.getParameterizedAttributeValue( + "AXNextTextMarkerForTextMarker", + marker + ), + "Iterated through all markers" + ); + + // Iterate backward with "AXPreviousTextMarkerForTextMarker" + marker = macDoc.getAttributeValue("AXEndTextMarker"); + while (marker) { + count--; + let index = macDoc.getParameterizedAttributeValue( + "AXIndexForTextMarker", + marker + ); + is( + index, + count, + `Correct index in "AXPreviousTextMarkerForTextMarker": ${count}` + ); + marker = macDoc.getParameterizedAttributeValue( + "AXPreviousTextMarkerForTextMarker", + marker + ); + } + + is(count, 0, "Iterated backward through all text markers"); +} + +addAccessibleTask("mac/doc_textmarker_test.html", async (browser, accDoc) => { + const expectedMarkerValues = await SpecialPowers.spawn( + browser, + [], + async () => { + return content.wrappedJSObject.EXPECTED; + } + ); + + testMarkerIntegrity(accDoc, expectedMarkerValues); +}); + +// Test text marker lesser-than operator +addAccessibleTask( + `<p id="p">hello <a id="a" href="#">goodbye</a> world</p>`, + async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let start = macDoc.getParameterizedAttributeValue( + "AXTextMarkerForIndex", + 1 + ); + let end = macDoc.getParameterizedAttributeValue("AXTextMarkerForIndex", 10); + + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [end, start] + ); + is(stringForRange(macDoc, range), "ello good"); + } +); + +addAccessibleTask( + `<input id="input" value=""><a href="#">goodbye</a>`, + async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let input = getNativeInterface(accDoc, "input"); + + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUIElement", + input + ); + + is(stringForRange(macDoc, range), "", "string value is correct"); + } +); diff --git a/accessible/tests/browser/mac/browser_text_input.js b/accessible/tests/browser/mac/browser_text_input.js new file mode 100644 index 0000000000..b5aa9511a1 --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_input.js @@ -0,0 +1,412 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +function testValueChangedEventData( + macIface, + data, + expectedId, + expectedChangeValue, + expectedEditType, + expectedWordAtLeft +) { + is( + data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"), + expectedId, + "Correct AXTextChangeElement" + ); + is( + data.AXTextStateChangeType, + AXTextStateChangeTypeEdit, + "Correct AXTextStateChangeType" + ); + + let changeValues = data.AXTextChangeValues; + is(changeValues.length, 1, "One element in AXTextChangeValues"); + is( + changeValues[0].AXTextChangeValue, + expectedChangeValue, + "Correct AXTextChangeValue" + ); + is( + changeValues[0].AXTextEditType, + expectedEditType, + "Correct AXTextEditType" + ); + + let textMarker = changeValues[0].AXTextChangeValueStartMarker; + ok(textMarker, "There is a AXTextChangeValueStartMarker"); + let range = macIface.getParameterizedAttributeValue( + "AXLeftWordTextMarkerRangeForTextMarker", + textMarker + ); + let str = macIface.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + range, + "correct word before caret" + ); + is(str, expectedWordAtLeft); +} + +// Return true if the first given object a subset of the second +function isSubset(subset, superset) { + if (typeof subset != "object" || typeof superset != "object") { + return superset == subset; + } + + for (let [prop, val] of Object.entries(subset)) { + if (!isSubset(val, superset[prop])) { + return false; + } + } + + return true; +} + +function matchWebArea(expectedId, expectedInfo) { + return (iface, data) => { + if (!data) { + return false; + } + + let textChangeElemID = data.AXTextChangeElement.getAttributeValue( + "AXDOMIdentifier" + ); + + return ( + iface.getAttributeValue("AXRole") == "AXWebArea" && + textChangeElemID == expectedId && + isSubset(expectedInfo, data) + ); + }; +} + +function matchInput(expectedId, expectedInfo) { + return (iface, data) => { + if (!data) { + return false; + } + + return ( + iface.getAttributeValue("AXDOMIdentifier") == expectedId && + isSubset(expectedInfo, data) + ); + }; +} + +async function synthKeyAndTestSelectionChanged( + synthKey, + synthEvent, + expectedId, + expectedSelectionString, + expectedSelectionInfo +) { + let selectionChangedEvents = Promise.all([ + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchWebArea(expectedId, expectedSelectionInfo) + ), + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchInput(expectedId, expectedSelectionInfo) + ), + ]); + + EventUtils.synthesizeKey(synthKey, synthEvent); + let [webareaEvent, inputEvent] = await selectionChangedEvents; + is( + inputEvent.data.AXTextChangeElement.getAttributeValue("AXDOMIdentifier"), + expectedId, + "Correct AXTextChangeElement" + ); + + let rangeString = inputEvent.macIface.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + inputEvent.data.AXSelectedTextMarkerRange + ); + is( + rangeString, + expectedSelectionString, + `selection has correct value (${expectedSelectionString})` + ); + + is( + webareaEvent.macIface.getAttributeValue("AXDOMIdentifier"), + "body", + "Input event target is top-level WebArea" + ); + rangeString = webareaEvent.macIface.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + inputEvent.data.AXSelectedTextMarkerRange + ); + is( + rangeString, + expectedSelectionString, + `selection has correct value (${expectedSelectionString}) via top document` + ); +} + +async function synthKeyAndTestValueChanged( + synthKey, + synthEvent, + expectedId, + expectedTextSelectionId, + expectedChangeValue, + expectedEditType, + expectedWordAtLeft +) { + let valueChangedEvents = Promise.all([ + waitForMacEvent( + "AXSelectedTextChanged", + matchWebArea(expectedTextSelectionId, { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + }) + ), + waitForMacEvent( + "AXSelectedTextChanged", + matchInput(expectedTextSelectionId, { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + }) + ), + waitForMacEventWithInfo( + "AXValueChanged", + matchWebArea(expectedId, { + AXTextStateChangeType: AXTextStateChangeTypeEdit, + AXTextChangeValues: [ + { + AXTextChangeValue: expectedChangeValue, + AXTextEditType: expectedEditType, + }, + ], + }) + ), + waitForMacEventWithInfo( + "AXValueChanged", + matchInput(expectedId, { + AXTextStateChangeType: AXTextStateChangeTypeEdit, + AXTextChangeValues: [ + { + AXTextChangeValue: expectedChangeValue, + AXTextEditType: expectedEditType, + }, + ], + }) + ), + ]); + + EventUtils.synthesizeKey(synthKey, synthEvent); + let [, , webareaEvent, inputEvent] = await valueChangedEvents; + + testValueChangedEventData( + webareaEvent.macIface, + webareaEvent.data, + expectedId, + expectedChangeValue, + expectedEditType, + expectedWordAtLeft + ); + testValueChangedEventData( + inputEvent.macIface, + inputEvent.data, + expectedId, + expectedChangeValue, + expectedEditType, + expectedWordAtLeft + ); +} + +async function focusIntoInputAndType(accDoc, inputId, innerContainerId) { + let selectionId = innerContainerId ? innerContainerId : inputId; + let input = getNativeInterface(accDoc, inputId); + ok(!input.getAttributeValue("AXFocused"), "input is not focused"); + ok(input.isAttributeSettable("AXFocused"), "input is focusable"); + let events = Promise.all([ + waitForMacEvent( + "AXFocusedUIElementChanged", + iface => iface.getAttributeValue("AXDOMIdentifier") == inputId + ), + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchWebArea(selectionId, { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + }) + ), + waitForMacEventWithInfo( + "AXSelectedTextChanged", + matchInput(selectionId, { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + }) + ), + ]); + input.setAttributeValue("AXFocused", true); + await events; + + async function testTextInput( + synthKey, + expectedChangeValue, + expectedWordAtLeft + ) { + await synthKeyAndTestValueChanged( + synthKey, + null, + inputId, + selectionId, + expectedChangeValue, + AXTextEditTypeTyping, + expectedWordAtLeft + ); + } + + await testTextInput("h", "h", "h"); + await testTextInput("e", "e", "he"); + await testTextInput("l", "l", "hel"); + await testTextInput("l", "l", "hell"); + await testTextInput("o", "o", "hello"); + await testTextInput(" ", " ", "hello"); + // You would expect this to be useless but this is what VO + // consumes. I guess it concats the inserted text data to the + // word to the left of the marker. + await testTextInput("w", "w", " "); + await testTextInput("o", "o", "wo"); + await testTextInput("r", "r", "wor"); + await testTextInput("l", "l", "worl"); + await testTextInput("d", "d", "world"); + + async function testTextDelete(expectedChangeValue, expectedWordAtLeft) { + await synthKeyAndTestValueChanged( + "KEY_Backspace", + null, + inputId, + selectionId, + expectedChangeValue, + AXTextEditTypeDelete, + expectedWordAtLeft + ); + } + + await testTextDelete("d", "worl"); + await testTextDelete("l", "wor"); + + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + null, + selectionId, + "", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionMove, + AXTextSelectionDirection: AXTextSelectionDirectionPrevious, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + { shiftKey: true }, + selectionId, + "o", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, + AXTextSelectionDirection: AXTextSelectionDirectionPrevious, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + { shiftKey: true }, + selectionId, + "wo", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, + AXTextSelectionDirection: AXTextSelectionDirectionPrevious, + AXTextSelectionGranularity: AXTextSelectionGranularityCharacter, + } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + null, + selectionId, + "", + { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove } + ); + await synthKeyAndTestSelectionChanged( + "KEY_Home", + { shiftKey: true }, + selectionId, + "hello ", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, + AXTextSelectionDirection: AXTextSelectionDirectionPrevious, + AXTextSelectionGranularity: AXTextSelectionGranularityWord, + } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowLeft", + null, + selectionId, + "", + { AXTextStateChangeType: AXTextStateChangeTypeSelectionMove } + ); + await synthKeyAndTestSelectionChanged( + "KEY_ArrowRight", + { shiftKey: true, altKey: true }, + selectionId, + "hello", + { + AXTextStateChangeType: AXTextStateChangeTypeSelectionExtend, + AXTextSelectionDirection: AXTextSelectionDirectionNext, + AXTextSelectionGranularity: AXTextSelectionGranularityWord, + } + ); +} + +// Test text input +addAccessibleTask( + `<a href="#">link</a> <input id="input">`, + async (browser, accDoc) => { + await focusIntoInputAndType(accDoc, "input"); + } +); + +// Test content editable +addAccessibleTask( + `<div id="input" contentEditable="true" tabindex="0" role="textbox" aria-multiline="true"><div id="inner"><br /></div></div>`, + async (browser, accDoc) => { + const inner = getNativeInterface(accDoc, "inner"); + const editableAncestor = inner.getAttributeValue("AXEditableAncestor"); + is( + editableAncestor.getAttributeValue("AXDOMIdentifier"), + "input", + "Editable ancestor is input" + ); + await focusIntoInputAndType(accDoc, "input"); + } +); + +// Test text input in iframe +addAccessibleTask( + `<a href="#">link</a> <input id="input">`, + async (browser, accDoc) => { + await focusIntoInputAndType(accDoc, "input"); + }, + { iframe: true } +); + +// Test input that gets role::EDITCOMBOBOX +addAccessibleTask(`<input type="text" id="box">`, async (browser, accDoc) => { + const box = getNativeInterface(accDoc, "box"); + const editableAncestor = box.getAttributeValue("AXEditableAncestor"); + is( + editableAncestor.getAttributeValue("AXDOMIdentifier"), + "box", + "Editable ancestor is box itself" + ); + await focusIntoInputAndType(accDoc, "box"); +}); diff --git a/accessible/tests/browser/mac/browser_text_leaf.js b/accessible/tests/browser/mac/browser_text_leaf.js new file mode 100644 index 0000000000..c7c7a5c319 --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_leaf.js @@ -0,0 +1,75 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +/** + * Test accessibles aren't created for linebreaks. + */ +addAccessibleTask(`hello<br>world`, async (browser, accDoc) => { + let doc = accDoc.nativeInterface.QueryInterface(Ci.nsIAccessibleMacInterface); + let docChildren = doc.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The document contains a root group"); + + let rootGroup = docChildren[0]; + let children = rootGroup.getAttributeValue("AXChildren"); + is(docChildren.length, 1, "The root group contains 2 children"); + + // verify first child is correct + is( + children[0].getAttributeValue("AXRole"), + "AXStaticText", + "First child is a text node" + ); + is( + children[0].getAttributeValue("AXValue"), + "hello", + "First child is hello text" + ); + + // verify second child is correct + is( + children[1].getAttributeValue("AXRole"), + "AXStaticText", + "Second child is a text node" + ); + + is( + children[1].getAttributeValue("AXValue"), + "world ", + "Second child is world text" + ); + // we have a trailing space here due to bug 1577028 +}); + +addAccessibleTask( + `<p id="p">hello, this is a test</p>`, + async (browser, accDoc) => { + let p = getNativeInterface(accDoc, "p"); + let textLeaf = p.getAttributeValue("AXChildren")[0]; + ok(textLeaf, "paragraph has a text leaf"); + + let str = textLeaf.getParameterizedAttributeValue( + "AXStringForRange", + NSRange(3, 6) + ); + + is(str, "lo, this ", "AXStringForRange matches."); + + let smallBounds = textLeaf.getParameterizedAttributeValue( + "AXBoundsForRange", + NSRange(3, 6) + ); + + let largeBounds = textLeaf.getParameterizedAttributeValue( + "AXBoundsForRange", + NSRange(3, 8) + ); + + ok(smallBounds.size[0] < largeBounds.size[0], "longer range is wider"); + } +); diff --git a/accessible/tests/browser/mac/browser_text_selection.js b/accessible/tests/browser/mac/browser_text_selection.js new file mode 100644 index 0000000000..a914adba8e --- /dev/null +++ b/accessible/tests/browser/mac/browser_text_selection.js @@ -0,0 +1,187 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/** + * Test simple text selection + */ +addAccessibleTask(`<p id="p">Hello World</p>`, async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let startMarker = macDoc.getAttributeValue("AXStartTextMarker"); + let endMarker = macDoc.getAttributeValue("AXEndTextMarker"); + let range = macDoc.getParameterizedAttributeValue( + "AXTextMarkerRangeForUnorderedTextMarkers", + [startMarker, endMarker] + ); + is(stringForRange(macDoc, range), "Hello World"); + + let evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + !info.AXTextStateSync && + info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend && + elem.getAttributeValue("AXRole") == "AXWebArea" + ); + }); + await SpecialPowers.spawn(browser, [], () => { + let p = content.document.getElementById("p"); + let r = new content.Range(); + r.setStart(p.firstChild, 1); + r.setEnd(p.firstChild, 8); + + let s = content.getSelection(); + s.addRange(r); + }); + await evt; + + range = macDoc.getAttributeValue("AXSelectedTextMarkerRange"); + is(stringForRange(macDoc, range), "ello Wo"); + + let firstWordRange = macDoc.getParameterizedAttributeValue( + "AXRightWordTextMarkerRangeForTextMarker", + startMarker + ); + is(stringForRange(macDoc, firstWordRange), "Hello"); + + evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + !info.AXTextStateSync && + info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend && + elem.getAttributeValue("AXRole") == "AXWebArea" + ); + }); + macDoc.setAttributeValue("AXSelectedTextMarkerRange", firstWordRange); + await evt; + range = macDoc.getAttributeValue("AXSelectedTextMarkerRange"); + is(stringForRange(macDoc, range), "Hello"); + + // Collapse selection + evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + info.AXTextStateSync && + info.AXTextStateChangeType == AXTextStateChangeTypeSelectionMove && + elem.getAttributeValue("AXRole") == "AXWebArea" + ); + }); + await SpecialPowers.spawn(browser, [], () => { + let s = content.getSelection(); + s.collapseToEnd(); + }); + await evt; +}); + +/** + * Test text selection events caused by focus change + */ +addAccessibleTask( + `<p> + Hello <a href="#" id="link">World</a>, + I <a href="#" style="user-select: none;" id="unselectable_link">love</a> + <button id="button">you</button></p>`, + async (browser, accDoc) => { + // Set up an AXSelectedTextChanged listener here. It will get resolved + // on the first non-root event it encounters, so if we test its data at the end + // of this test it will show us the first text-selectable object that was focused, + // which is "link". + let selTextChanged = waitForMacEvent( + "AXSelectedTextChanged", + e => e.getAttributeValue("AXDOMIdentifier") != "body" + ); + + let focusChanged = waitForMacEvent("AXFocusedUIElementChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("unselectable_link").focus(); + }); + let focusChangedTarget = await focusChanged; + is( + focusChangedTarget.getAttributeValue("AXDOMIdentifier"), + "unselectable_link", + "Correct event target" + ); + + focusChanged = waitForMacEvent("AXFocusedUIElementChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("button").focus(); + }); + focusChangedTarget = await focusChanged; + is( + focusChangedTarget.getAttributeValue("AXDOMIdentifier"), + "button", + "Correct event target" + ); + + focusChanged = waitForMacEvent("AXFocusedUIElementChanged"); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("link").focus(); + }); + focusChangedTarget = await focusChanged; + is( + focusChangedTarget.getAttributeValue("AXDOMIdentifier"), + "link", + "Correct event target" + ); + + let selTextChangedTarget = await selTextChanged; + is( + selTextChangedTarget.getAttributeValue("AXDOMIdentifier"), + "link", + "Correct event target" + ); + } +); + +/** + * Test text selection with focus change + */ +addAccessibleTask( + `<p id="p">Hello <input id="input"></p>`, + async (browser, accDoc) => { + let macDoc = accDoc.nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); + + let evt = waitForMacEventWithInfo("AXSelectedTextChanged", (elem, info) => { + return ( + !info.AXTextStateSync && + info.AXTextStateChangeType == AXTextStateChangeTypeSelectionExtend && + elem.getAttributeValue("AXRole") == "AXWebArea" + ); + }); + await SpecialPowers.spawn(browser, [], () => { + let p = content.document.getElementById("p"); + let r = new content.Range(); + r.setStart(p.firstChild, 1); + r.setEnd(p.firstChild, 3); + + let s = content.getSelection(); + s.addRange(r); + }); + await evt; + + let range = macDoc.getAttributeValue("AXSelectedTextMarkerRange"); + is(stringForRange(macDoc, range), "el"); + + let events = Promise.all([ + waitForMacEvent("AXFocusedUIElementChanged"), + waitForMacEventWithInfo("AXSelectedTextChanged"), + ]); + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("input").focus(); + }); + let [, { data }] = await events; + ok( + data.AXTextSelectionChangedFocus, + "have AXTextSelectionChangedFocus in event info" + ); + ok(!data.AXTextStateSync, "no AXTextStateSync in editables"); + is( + data.AXTextSelectionDirection, + AXTextSelectionDirectionDiscontiguous, + "discontigous direction" + ); + } +); diff --git a/accessible/tests/browser/mac/browser_toggle_radio_check.js b/accessible/tests/browser/mac/browser_toggle_radio_check.js new file mode 100644 index 0000000000..17c292fd87 --- /dev/null +++ b/accessible/tests/browser/mac/browser_toggle_radio_check.js @@ -0,0 +1,140 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/** + * Test input[type=checkbox] + */ +addAccessibleTask( + `<input type="checkbox" id="vehicle"><label for="vehicle"> Bike</label>`, + async (browser, accDoc) => { + let checkbox = getNativeInterface(accDoc, "vehicle"); + is(checkbox.getAttributeValue("AXValue"), 0, "Correct initial value"); + + let actions = checkbox.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = waitForMacEvent("AXValueChanged", "vehicle"); + checkbox.performAction("AXPress"); + await evt; + is(checkbox.getAttributeValue("AXValue"), 1, "Correct checked value"); + + evt = waitForMacEvent("AXValueChanged", "vehicle"); + checkbox.performAction("AXPress"); + await evt; + is(checkbox.getAttributeValue("AXValue"), 0, "Correct checked value"); + } +); + +/** + * Test aria-pressed toggle buttons + */ +addAccessibleTask( + `<button id="toggle" aria-pressed="false">toggle</button>`, + async (browser, accDoc) => { + // Set up a callback to change the toggle value + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("toggle").onclick = e => { + let curVal = e.target.getAttribute("aria-pressed"); + let nextVal = curVal == "false" ? "true" : "false"; + e.target.setAttribute("aria-pressed", nextVal); + }; + }); + + let toggle = getNativeInterface(accDoc, "toggle"); + is(toggle.getAttributeValue("AXValue"), 0, "Correct initial value"); + + let actions = toggle.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = waitForMacEvent("AXValueChanged", "toggle"); + toggle.performAction("AXPress"); + await evt; + is(toggle.getAttributeValue("AXValue"), 1, "Correct checked value"); + + evt = waitForMacEvent("AXValueChanged", "toggle"); + toggle.performAction("AXPress"); + await evt; + is(toggle.getAttributeValue("AXValue"), 0, "Correct checked value"); + } +); + +/** + * Test aria-checked with tri state + */ +addAccessibleTask( + `<button role="checkbox" id="checkbox" aria-checked="false">toggle</button>`, + async (browser, accDoc) => { + // Set up a callback to change the toggle value + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("checkbox").onclick = e => { + const states = ["false", "true", "mixed"]; + let currState = e.target.getAttribute("aria-checked"); + let nextState = states[(states.indexOf(currState) + 1) % states.length]; + e.target.setAttribute("aria-checked", nextState); + }; + }); + + let checkbox = getNativeInterface(accDoc, "checkbox"); + is(checkbox.getAttributeValue("AXValue"), 0, "Correct initial value"); + + let actions = checkbox.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = waitForMacEvent("AXValueChanged", "checkbox"); + checkbox.performAction("AXPress"); + await evt; + is(checkbox.getAttributeValue("AXValue"), 1, "Correct checked value"); + + evt = waitForMacEvent("AXValueChanged", "checkbox"); + checkbox.performAction("AXPress"); + await evt; + is(checkbox.getAttributeValue("AXValue"), 2, "Correct checked value"); + } +); + +/** + * Test input[type=radio] + */ +addAccessibleTask( + `<input type="radio" id="huey" name="drone" value="huey" checked> + <label for="huey">Huey</label> + <input type="radio" id="dewey" name="drone" value="dewey"> + <label for="dewey">Dewey</label>`, + async (browser, accDoc) => { + let huey = getNativeInterface(accDoc, "huey"); + is(huey.getAttributeValue("AXValue"), 1, "Correct initial value for huey"); + + let dewey = getNativeInterface(accDoc, "dewey"); + is( + dewey.getAttributeValue("AXValue"), + 0, + "Correct initial value for dewey" + ); + + let actions = dewey.actionNames; + ok(actions.includes("AXPress"), "Has press action"); + + let evt = Promise.all([ + waitForMacEvent("AXValueChanged", "huey"), + waitForMacEvent("AXValueChanged", "dewey"), + ]); + dewey.performAction("AXPress"); + await evt; + is( + dewey.getAttributeValue("AXValue"), + 1, + "Correct checked value for dewey" + ); + is(huey.getAttributeValue("AXValue"), 0, "Correct checked value for huey"); + } +); diff --git a/accessible/tests/browser/mac/browser_webarea.js b/accessible/tests/browser/mac/browser_webarea.js new file mode 100644 index 0000000000..c1e68dac1b --- /dev/null +++ b/accessible/tests/browser/mac/browser_webarea.js @@ -0,0 +1,76 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +// Test web area role and AXLoadComplete event +addAccessibleTask(``, async (browser, accDoc) => { + let evt = waitForMacEvent("AXLoadComplete"); + await SpecialPowers.spawn(browser, [], () => { + content.location = "data:text/html,<title>webarea test</title>"; + }); + let doc = await evt; + + is( + doc.getAttributeValue("AXRole"), + "AXWebArea", + "document has AXWebArea role" + ); + is(doc.getAttributeValue("AXValue"), "", "document has no AXValue"); + is(doc.getAttributeValue("AXTitle"), null, "document has no AXTitle"); + is( + doc.getAttributeValue("AXDescription"), + "webarea test", + "test has correct label" + ); + + is(doc.getAttributeValue("AXLoaded"), 1, "document has finished loading"); +}); + +// Test iframe web area role and AXLayoutComplete event +addAccessibleTask(`<title>webarea test</title>`, async (browser, accDoc) => { + // If the iframe loads before the top level document finishes loading, we'll + // get both an AXLayoutComplete event for the iframe and an AXLoadComplete + // event for the document. Otherwise, if the iframe loads after the + // document, we'll get one AXLoadComplete event. + let eventPromise = Promise.race([ + waitForMacEvent("AXLayoutComplete"), + waitForMacEvent("AXLoadComplete"), + ]); + await SpecialPowers.spawn(browser, [], () => { + const iframe = content.document.createElement("iframe"); + iframe.src = "data:text/html,hello world"; + content.document.body.appendChild(iframe); + }); + let doc = await eventPromise; + + if (doc.getAttributeValue("AXTitle")) { + // iframe should have no title, so if we get a title here + // we've got the main document and need to get the iframe from + // the main doc + doc = doc.getAttributeValue("AXChildren")[0]; + } + + is( + doc.getAttributeValue("AXRole"), + "AXWebArea", + "iframe document has AXWebArea role" + ); + is(doc.getAttributeValue("AXValue"), "", "iframe document has no AXValue"); + is(doc.getAttributeValue("AXTitle"), null, "iframe document has no AXTitle"); + is( + doc.getAttributeValue("AXDescription"), + "data:text/html,hello world", + "test has correct label" + ); + + is( + doc.getAttributeValue("AXLoaded"), + 1, + "iframe document has finished loading" + ); +}); diff --git a/accessible/tests/browser/mac/doc_aria_tabs.html b/accessible/tests/browser/mac/doc_aria_tabs.html new file mode 100644 index 0000000000..9de6f8b81c --- /dev/null +++ b/accessible/tests/browser/mac/doc_aria_tabs.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<html><head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <meta charset="utf-8"> + + <style type="text/css"> + .tabs { + padding: 1em; + } + + [role="tablist"] { + margin-bottom: -1px; + } + + [role="tab"] { + position: relative; + z-index: 1; + background: white; + border-radius: 5px 5px 0 0; + border: 1px solid grey; + border-bottom: 0; + padding: 0.2em; + } + + [role="tab"][aria-selected="true"] { + z-index: 3; + } + + [role="tabpanel"] { + position: relative; + padding: 0 0.5em 0.5em 0.7em; + border: 1px solid grey; + border-radius: 0 0 5px 5px; + background: white; + z-index: 2; + } + + [role="tabpanel"]:focus { + border-color: orange; + outline: 1px solid orange; + } + </style> + <script> + 'use strict'; + /* exported changeTabs */ + function changeTabs(target) { + const parent = target.parentNode; + const grandparent = parent.parentNode; + + // Remove all current selected tabs + parent + .querySelectorAll('[aria-selected="true"]') + .forEach(t => t.setAttribute("aria-selected", false)); + + // Set this tab as selected + target.setAttribute("aria-selected", true); + + // Hide all tab panels + grandparent + .querySelectorAll('[role="tabpanel"]') + .forEach(p => p.setAttribute("hidden", true)); + + // Show the selected panel + grandparent.parentNode + .querySelector(`#${target.getAttribute("aria-controls")}`) + .removeAttribute("hidden"); + } + </script> + <title>ARIA: tab role - Example - code sample</title> +</head> +<body id="body"> + + <div class="tabs"> + <div id="tablist" role="tablist" aria-label="Sample Tabs"> + <button onclick="changeTabs(this)" role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1"> + First Tab + </button> + <button onclick="changeTabs(this)" role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2"> + Second Tab + </button> + <button onclick="changeTabs(this)" role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3"> + Third Tab + </button> + </div> + <div id="panel-1" role="tabpanel" tabindex="0" aria-labelledby="tab-1"> + <p>Content for the first panel</p> + </div> + <div id="panel-2" role="tabpanel" tabindex="0" aria-labelledby="tab-2" hidden=""> + <p>Content for the second panel</p> + </div> + <div id="panel-3" role="tabpanel" tabindex="0" aria-labelledby="tab-3" hidden=""> + <p>Content for the third panel</p> + </div> + </div> +</body></html> diff --git a/accessible/tests/browser/mac/doc_menulist.xhtml b/accessible/tests/browser/mac/doc_menulist.xhtml new file mode 100644 index 0000000000..d6751bc8f4 --- /dev/null +++ b/accessible/tests/browser/mac/doc_menulist.xhtml @@ -0,0 +1,19 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <hbox> + <label control="defaultZoom" value="Zoom"/> + <hbox> + <menulist id="defaultZoom"> + <menupopup> + <menuitem label="50%" value="50"/> + <menuitem label="100%" value="100"/> + <menuitem label="150%" value="150"/> + <menuitem label="200%" value="200"/> + </menupopup> + </menulist> + </hbox> + </hbox> +</window> diff --git a/accessible/tests/browser/mac/doc_rich_listbox.xhtml b/accessible/tests/browser/mac/doc_rich_listbox.xhtml new file mode 100644 index 0000000000..3acaf3bff8 --- /dev/null +++ b/accessible/tests/browser/mac/doc_rich_listbox.xhtml @@ -0,0 +1,22 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <richlistbox id="categories"> + <richlistitem id="general"> + <label value="general"/> + </richlistitem> + + <richlistitem id="home"> + <label value="home"/> + </richlistitem> + + <richlistitem id="search"> + <label value="search"/> + </richlistitem> + + <richlistitem id="privacy"> + <label value="privacy"/> + </richlistitem> + </richlistbox> +</window> diff --git a/accessible/tests/browser/mac/doc_textmarker_test.html b/accessible/tests/browser/mac/doc_textmarker_test.html new file mode 100644 index 0000000000..8a73c95a35 --- /dev/null +++ b/accessible/tests/browser/mac/doc_textmarker_test.html @@ -0,0 +1,2424 @@ +<!DOCTYPE html> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <meta charset="utf-8"> + </head> + <body id="body"> + <p>Bob Loblaw Lobs Law Bomb</p> + <p>I love all of my <a href="#">children</a> equally</p> + <p>This is the <b>best</b> free scr<a href="#">apbook</a>ing class I have ever taken</p> + <ul> + <li>Fried cheese with club sauce</li> + <li>Popcorn shrimp with club sauce</li> + <li>Chicken fingers with <i>spicy</i> club sauce</li> + </ul> + <ul style="list-style: none;"><li>Do not order the Skip's Scramble</li></ul> + <p style="width: 1rem">These are my awards, Mother. From Army.</p> + <p>I <input value="deceived you">, mom.</p> + <script> + "use strict"; + window.EXPECTED = [ + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bob", "Bob"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bob", "Bob"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bob", "Bob"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bob", " "], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: [" ", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", "Loblaw"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Loblaw", " "], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: [" ", "Lobs"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Lobs", "Lobs"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Lobs", "Lobs"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Lobs", "Lobs"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Lobs", " "], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: [" ", "Law"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Law", "Law"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Law", "Law"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Law", " "], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: [" ", "Bomb"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bomb", "Bomb"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bomb", "Bomb"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "Bob Loblaw Lobs Law Bomb", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"], + words: ["Bomb", "Bomb"], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "Bob Loblaw Lobs Law Bomb", + paragraph: "I love all of my children equally", + lines: ["Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb", + "I love all of my children equally"], + words: ["Bomb", ""], + element: ["AXStaticText", + "Bob Loblaw Lobs Law Bomb", + "Bob Loblaw Lobs Law Bomb"] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["I", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "love"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["love", "love"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["love", "love"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["love", "love"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["love", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "all"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["all", "all"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["all", "all"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["all", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "of"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["of", "of"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["of", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "my"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["my", "my"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["my", " "], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "I love all of my ", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "children"], + element: ["AXStaticText", "I love all of my ", "I love all of my "] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", "children"], + element: ["AXStaticText", "children", "children"] }, + { style: "children", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["children", " "], + element: ["AXStaticText", "children", "children"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: [" ", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "I love all of my children equally", + lines: ["I love all of my children equally", + "I love all of my children equally", + "I love all of my children equally"], + words: ["equally", "equally"], + element: ["AXStaticText", " equally", " equally"] }, + { style: " equally", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["I love all of my children equally", + "I love all of my children equally", + "This is the best free scrapbooking class I have ever taken"], + words: ["equally", ""], + element: ["AXStaticText", " equally", " equally"] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["This", "This"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["This", "This"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["This", "This"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["This", " "], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "is"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["is", "is"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["is", " "], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "the"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["the", "the"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["the", "the"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["the", " "], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "This is the ", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "best"], + element: ["AXStaticText", "This is the ", "This is the "] }, + { style: "best", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["best", "best"], + element: ["AXStaticText", "best", "best"] }, + { style: "best", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["best", "best"], + element: ["AXStaticText", "best", "best"] }, + { style: "best", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["best", "best"], + element: ["AXStaticText", "best", "best"] }, + { style: "best", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["best", " "], + element: ["AXStaticText", "best", "best"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "free"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["free", "free"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["free", "free"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["free", "free"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["free", " "], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "scrapbooking"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: " free scr", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", " free scr", " free scr"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "apbook", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", "apbook", "apbook"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", "scrapbooking"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["scrapbooking", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", "class"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["class", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "I"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["I", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "have"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["have", "have"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["have", "have"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["have", "have"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["have", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "ever"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["ever", "ever"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["ever", "ever"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["ever", "ever"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["ever", " "], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: [" ", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["taken", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["taken", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["taken", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "This is the best free scrapbooking class I have ever taken", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken"], + words: ["taken", "taken"], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "ing class I have ever taken", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["This is the best free scrapbooking class I have ever taken", + "This is the best free scrapbooking class I have ever taken", + "\u2022 Fried cheese with club sauce"], + words: ["taken", ""], + element: ["AXStaticText", + "ing class I have ever taken", + "ing class I have ever taken"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", "\u2022 Fried"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", "\u2022 Fried"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", "\u2022 Fried"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", "\u2022 Fried"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["\u2022 Fried", " "], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: [" ", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", "cheese"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["cheese", " "], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: [" ", "with"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["with", " "], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: [" ", "club"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["club", " "], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: [" ", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Fried cheese with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Fried cheese with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", ""], + element: ["AXStaticText", + "Fried cheese with club sauce", + "\u2022 Fried cheese with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", "\u2022 Popcorn"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["\u2022 Popcorn", " "], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: [" ", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", "shrimp"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["shrimp", " "], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: [" ", "with"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["with", " "], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: [" ", "club"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["club", "club"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["club", " "], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: [" ", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Popcorn shrimp with club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Popcorn shrimp with club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", ""], + element: ["AXStaticText", + "Popcorn shrimp with club sauce", + "\u2022 Popcorn shrimp with club sauce"] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", "\u2022 Chicken"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["\u2022 Chicken", " "], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", "fingers"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["fingers", " "], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "with"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["with", "with"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["with", " "], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "\u2022 Chicken fingers with ", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "spicy"], + element: ["AXStaticText", + "Chicken fingers with ", + "\u2022 Chicken fingers with "] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", "spicy"], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", "spicy"], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", "spicy"], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", "spicy"], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: "spicy", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["spicy", " "], + element: ["AXStaticText", "spicy", "spicy"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "club"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["club", "club"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["club", "club"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["club", "club"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["club", " "], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: [" ", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "\u2022 Chicken fingers with spicy club sauce", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce"], + words: ["sauce", "sauce"], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: " club sauce", + paragraph: "Do not order the Skip's Scramble", + lines: ["\u2022 Chicken fingers with spicy club sauce", + "\u2022 Chicken fingers with spicy club sauce", + "Do not order the Skip's Scramble"], + words: ["sauce", ""], + element: ["AXStaticText", " club sauce", " club sauce"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Do", "Do"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Do", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "not"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["not", "not"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["not", "not"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["not", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", "order"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["order", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "the"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["the", "the"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["the", "the"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["the", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "Skip'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "'"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Skip'", "s"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["s", " "], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: [" ", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "Do not order the Skip's Scramble", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"], + words: ["Scramble", "Scramble"], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "Do not order the Skip's Scramble", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Do not order the Skip's Scramble", + "Do not order the Skip's Scramble", + "These "], + words: ["Scramble", ""], + element: ["AXStaticText", + "Do not order the Skip's Scramble", + "Do not order the Skip's Scramble"] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", "These"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", "These"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", "These"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", "These"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["These ", "These ", "These "], + words: ["These", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["are ", "are ", "are "], + words: [" ", "are"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["are ", "are ", "are "], + words: ["are", "are"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["are ", "are ", "are "], + words: ["are", "are"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["are ", "are ", "are "], + words: ["are", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["my ", "my ", "my "], + words: [" ", "my"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["my ", "my ", "my "], + words: ["my", "my"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["my ", "my ", "my "], + words: ["my", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: [" ", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards,"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", "awards, "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["awards, ", "awards, ", "awards, "], + words: ["awards,", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: [" ", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", "Mother. "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Mother. ", "Mother. ", "Mother. "], + words: ["Mother.", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: [" ", "From"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: ["From", "From"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: ["From", "From"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: ["From", "From"], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["From ", "From ", "From "], + words: ["From", " "], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: [" ", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: ["Army.", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: ["Army.", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: ["Army.", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "These are my awards, Mother. From Army.", + lines: ["Army.", "Army.", "Army."], + words: ["Army.", "Army."], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "These are my awards, Mother. From Army.", + paragraph: "I deceived you, mom.", + lines: ["Army.", "Army.", "I deceived you, mom."], + words: ["Army.", ""], + element: ["AXStaticText", + "These are my awards, Mother. From Army.", + "These are my awards, Mother. From Army."] }, + { style: "I ", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["I", " "], + element: ["AXStaticText", "I ", "I "] }, + { style: "I ", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: [" ", "deceived"], + element: ["AXStaticText", "I ", "I "] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", "deceived"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["deceived", " "], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: [" ", "you"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["you", "you"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["deceived you", "deceived you", "deceived you"], + words: ["you", "you"], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: "deceived you", + paragraph: "deceived you", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["you", ""], + element: ["AXTextField", "deceived you", "deceived you"] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: [",", " "], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: [", ", "mom."], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["mom.", "mom."], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["mom.", "mom."], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["mom.", "mom."], + element: ["AXStaticText", ", mom.", ", mom."] }, + { style: ", mom.", + paragraph: "I deceived you, mom.", + lines: ["I deceived you, mom.", "I deceived you, mom.", "I deceived you, mom."], + words: ["mom.", ""], + element: ["AXStaticText", ", mom.", ", mom."] }]; + </script> + </body> +</html> diff --git a/accessible/tests/browser/mac/doc_tree.xhtml b/accessible/tests/browser/mac/doc_tree.xhtml new file mode 100644 index 0000000000..d043fa8923 --- /dev/null +++ b/accessible/tests/browser/mac/doc_tree.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin/" type="text/css"?> + +<window + xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + <tree id="tree" hidecolumnpicker="true"> + <treecols> + <treecol primary="true" label="Groceries"/> + </treecols> + <treechildren id="internalTree"> + <treeitem id="fruits" container="true" open="true"> + <treerow> + <treecell label="Fruits"/> + </treerow> + <treechildren> + <treeitem id="apple"> + <treerow> + <treecell label="Apple"/> + </treerow> + </treeitem> + <treeitem id="orange"> + <treerow> + <treecell label="Orange"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem id="veggies" container="true" open="true"> + <treerow> + <treecell label="Veggies"/> + </treerow> + <treechildren> + <treeitem id="greenVeggies" container="true" open="true"> + <treerow> + <treecell label="Green Veggies"/> + </treerow> + <treechildren> + <treeitem id="spinach"> + <treerow> + <treecell label="Spinach"/> + </treerow> + </treeitem> + <treeitem id="peas"> + <treerow> + <treecell label="Peas"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + <treeitem id="squash"> + <treerow> + <treecell label="Squash"/> + </treerow> + </treeitem> + </treechildren> + </treeitem> + </treechildren> + </tree> +</window> diff --git a/accessible/tests/browser/mac/head.js b/accessible/tests/browser/mac/head.js new file mode 100644 index 0000000000..7f70bd4a4e --- /dev/null +++ b/accessible/tests/browser/mac/head.js @@ -0,0 +1,114 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* exported getNativeInterface, waitForMacEventWithInfo, waitForMacEvent, + NSRange, NSDictionary, stringForRange, AXTextStateChangeTypeEdit, + AXTextEditTypeDelete, AXTextEditTypeTyping, AXTextStateChangeTypeSelectionMove, + AXTextStateChangeTypeSelectionExtend, AXTextSelectionDirectionUnknown, + AXTextSelectionDirectionPrevious, AXTextSelectionDirectionNext, + AXTextSelectionDirectionDiscontiguous, AXTextSelectionGranularityUnknown, + AXTextSelectionGranularityCharacter, AXTextSelectionGranularityWord */ + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); + +// AXTextStateChangeType enum values +const AXTextStateChangeTypeEdit = 1; +const AXTextStateChangeTypeSelectionMove = 2; +const AXTextStateChangeTypeSelectionExtend = 3; + +// AXTextEditType enum values +const AXTextEditTypeDelete = 1; +const AXTextEditTypeTyping = 3; + +// AXTextSelectionDirection enum values +const AXTextSelectionDirectionUnknown = 0; +const AXTextSelectionDirectionPrevious = 3; +const AXTextSelectionDirectionNext = 4; +const AXTextSelectionDirectionDiscontiguous = 5; + +// AXTextSelectionGranularity enum values +const AXTextSelectionGranularityUnknown = 0; +const AXTextSelectionGranularityCharacter = 1; +const AXTextSelectionGranularityWord = 2; + +function getNativeInterface(accDoc, id) { + return findAccessibleChildByID(accDoc, id).nativeInterface.QueryInterface( + Ci.nsIAccessibleMacInterface + ); +} + +function waitForMacEventWithInfo(notificationType, filter) { + let filterFunc = (macIface, data) => { + if (!filter) { + return true; + } + + if (typeof filter == "function") { + return filter(macIface, data); + } + + return macIface.getAttributeValue("AXDOMIdentifier") == filter; + }; + + return new Promise(resolve => { + let eventObserver = { + observe(subject, topic, data) { + let macEvent = subject.QueryInterface(Ci.nsIAccessibleMacEvent); + if ( + data === notificationType && + filterFunc(macEvent.macIface, macEvent.data) + ) { + Services.obs.removeObserver(this, "accessible-mac-event"); + resolve(macEvent); + } + }, + }; + Services.obs.addObserver(eventObserver, "accessible-mac-event"); + }); +} + +function waitForMacEvent(notificationType, filter) { + return waitForMacEventWithInfo(notificationType, filter).then( + e => e.macIface + ); +} + +function NSRange(location, length) { + return { + valueType: "NSRange", + value: [location, length], + }; +} + +function NSDictionary(dict) { + return { + objectType: "NSDictionary", + object: dict, + }; +} + +function stringForRange(macDoc, range) { + if (!range) { + return ""; + } + + return macDoc.getParameterizedAttributeValue( + "AXStringForTextMarkerRange", + range + ); +} diff --git a/accessible/tests/browser/scroll/browser.ini b/accessible/tests/browser/scroll/browser.ini new file mode 100644 index 0000000000..1e24426535 --- /dev/null +++ b/accessible/tests/browser/scroll/browser.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/browser/*.jsm + !/accessible/tests/mochitest/*.js + +[browser_test_zoom_text.js] +skip-if = e10s && os == 'win' # bug 1372296 diff --git a/accessible/tests/browser/scroll/browser_test_zoom_text.js b/accessible/tests/browser/scroll/browser_test_zoom_text.js new file mode 100644 index 0000000000..be58361c7b --- /dev/null +++ b/accessible/tests/browser/scroll/browser_test_zoom_text.js @@ -0,0 +1,150 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/layout.js */ +loadScripts({ name: "layout.js", dir: MOCHITESTS_DIR }); + +async function waitForContentPaint(browser) { + await SpecialPowers.spawn(browser, [], () => { + return new Promise(function(r) { + content.requestAnimationFrame(() => content.setTimeout(r)); + }); + }); +} + +async function runTests(browser, accDoc) { + await loadContentScripts(browser, "Layout.jsm"); + + let paragraph = findAccessibleChildByID(accDoc, "paragraph", [ + nsIAccessibleText, + ]); + let offset = 64; // beginning of 4th stanza + + let [x /* ,y*/] = getPos(paragraph); + let [docX, docY] = getPos(accDoc); + + paragraph.scrollSubstringToPoint( + offset, + offset, + COORDTYPE_SCREEN_RELATIVE, + docX, + docY + ); + + await waitForContentPaint(browser); + testTextPos(paragraph, offset, [x, docY], COORDTYPE_SCREEN_RELATIVE); + + await SpecialPowers.spawn(browser, [], () => { + content.Layout.zoomDocument(content.document, 2.0); + }); + + paragraph = findAccessibleChildByID(accDoc, "paragraph2", [ + nsIAccessibleText, + ]); + offset = 52; // // beginning of 4th stanza + [x /* ,y*/] = getPos(paragraph); + paragraph.scrollSubstringToPoint( + offset, + offset, + COORDTYPE_SCREEN_RELATIVE, + docX, + docY + ); + + await waitForContentPaint(browser); + testTextPos(paragraph, offset, [x, docY], COORDTYPE_SCREEN_RELATIVE); +} + +/** + * Test caching of accessible object states + */ +addAccessibleTask( + ` + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br><hr> + <p id='paragraph'> + Пошел котик на торжок<br> + Купил котик пирожок<br> + Пошел котик на улочку<br> + Купил котик булочку<br> + </p> + <hr><br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br><hr> + <p id='paragraph2'> + Самому ли съесть<br> + Либо Сашеньке снесть<br> + Я и сам укушу<br> + Я и Сашеньке снесу<br> + </p> + <hr><br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br>`, + runTests +); diff --git a/accessible/tests/browser/scroll/head.js b/accessible/tests/browser/scroll/head.js new file mode 100644 index 0000000000..672aa46171 --- /dev/null +++ b/accessible/tests/browser/scroll/head.js @@ -0,0 +1,19 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); diff --git a/accessible/tests/browser/shared-head.js b/accessible/tests/browser/shared-head.js new file mode 100644 index 0000000000..b848e09932 --- /dev/null +++ b/accessible/tests/browser/shared-head.js @@ -0,0 +1,779 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../mochitest/common.js */ +/* import-globals-from ../mochitest/promisified-events.js */ + +/* exported Logger, MOCHITESTS_DIR, invokeSetAttribute, invokeFocus, + invokeSetStyle, getAccessibleDOMNodeID, getAccessibleTagName, + addAccessibleTask, findAccessibleChildByID, isDefunct, + CURRENT_CONTENT_DIR, loadScripts, loadContentScripts, snippetToURL, + Cc, Cu, arrayFromChildren, forceGC, contentSpawnMutation, + DEFAULT_IFRAME_ID, DEFAULT_IFRAME_DOC_BODY_ID, invokeContentTask, + matchContentDoc, currentContentDoc, getContentDPR, + waitForImageMap, getContentBoundsForDOMElm */ + +const CURRENT_FILE_DIR = "/browser/accessible/tests/browser/"; + +/** + * Current browser test directory path used to load subscripts. + */ +const CURRENT_DIR = `chrome://mochitests/content${CURRENT_FILE_DIR}`; +/** + * A11y mochitest directory where we find common files used in both browser and + * plain tests. + */ +const MOCHITESTS_DIR = + "chrome://mochitests/content/a11y/accessible/tests/mochitest/"; +/** + * A base URL for test files used in content. + */ +const CURRENT_CONTENT_DIR = `http://example.com${CURRENT_FILE_DIR}`; + +const LOADED_CONTENT_SCRIPTS = new Map(); + +const DEFAULT_CONTENT_DOC_BODY_ID = "body"; +const DEFAULT_IFRAME_ID = "default-iframe-id"; +const DEFAULT_IFRAME_DOC_BODY_ID = "default-iframe-body-id"; + +const HTML_MIME_TYPE = "text/html"; +const XHTML_MIME_TYPE = "application/xhtml+xml"; + +function loadHTMLFromFile(path) { + // Load the HTML to return in the response from file. + // Since it's relative to the cwd of the test runner, we start there and + // append to get to the actual path of the file. + const testHTMLFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile); + const dirs = path.split("/"); + for (let i = 0; i < dirs.length; i++) { + testHTMLFile.append(dirs[i]); + } + + const testHTMLFileStream = Cc[ + "@mozilla.org/network/file-input-stream;1" + ].createInstance(Ci.nsIFileInputStream); + testHTMLFileStream.init(testHTMLFile, -1, 0, 0); + const testHTML = NetUtil.readInputStreamToString( + testHTMLFileStream, + testHTMLFileStream.available() + ); + + return testHTML; +} + +let gIsIframe = false; +let gIsRemoteIframe = false; + +function currentContentDoc() { + return gIsIframe ? DEFAULT_IFRAME_DOC_BODY_ID : DEFAULT_CONTENT_DOC_BODY_ID; +} + +/** + * Accessible event match criteria based on the id of the current document + * accessible in test. + * + * @param {nsIAccessibleEvent} event + * Accessible event to be tested for a match. + * + * @return {Boolean} + * True if accessible event's accessible object ID matches current + * document accessible ID. + */ +function matchContentDoc(event) { + return getAccessibleDOMNodeID(event.accessible) === currentContentDoc(); +} + +/** + * Used to dump debug information. + */ +let Logger = { + /** + * Set up this variable to dump log messages into console. + */ + dumpToConsole: false, + + /** + * Set up this variable to dump log messages into error console. + */ + dumpToAppConsole: false, + + /** + * Return true if dump is enabled. + */ + get enabled() { + return this.dumpToConsole || this.dumpToAppConsole; + }, + + /** + * Dump information into console if applicable. + */ + log(msg) { + if (this.enabled) { + this.logToConsole(msg); + this.logToAppConsole(msg); + } + }, + + /** + * Log message to console. + */ + logToConsole(msg) { + if (this.dumpToConsole) { + dump(`\n${msg}\n`); + } + }, + + /** + * Log message to error console. + */ + logToAppConsole(msg) { + if (this.dumpToAppConsole) { + Services.console.logStringMessage(`${msg}`); + } + }, +}; + +/** + * Asynchronously set or remove content element's attribute (in content process + * if e10s is enabled). + * @param {Object} browser current "tabbrowser" element + * @param {String} id content element id + * @param {String} attr attribute name + * @param {String?} value optional attribute value, if not present, remove + * attribute + * @return {Promise} promise indicating that attribute is set/removed + */ +function invokeSetAttribute(browser, id, attr, value) { + if (value) { + Logger.log(`Setting ${attr} attribute to ${value} for node with id: ${id}`); + } else { + Logger.log(`Removing ${attr} attribute from node with id: ${id}`); + } + + return invokeContentTask( + browser, + [id, attr, value], + (contentId, contentAttr, contentValue) => { + let elm = content.document.getElementById(contentId); + if (contentValue) { + elm.setAttribute(contentAttr, contentValue); + } else { + elm.removeAttribute(contentAttr); + } + } + ); +} + +/** + * Asynchronously set or remove content element's style (in content process if + * e10s is enabled, or in fission process if fission is enabled and a fission + * frame is present). + * @param {Object} browser current "tabbrowser" element + * @param {String} id content element id + * @param {String} aStyle style property name + * @param {String?} aValue optional style property value, if not present, + * remove style + * @return {Promise} promise indicating that style is set/removed + */ +function invokeSetStyle(browser, id, style, value) { + if (value) { + Logger.log(`Setting ${style} style to ${value} for node with id: ${id}`); + } else { + Logger.log(`Removing ${style} style from node with id: ${id}`); + } + + return invokeContentTask( + browser, + [id, style, value], + (contentId, contentStyle, contentValue) => { + const elm = content.document.getElementById(contentId); + if (contentValue) { + elm.style[contentStyle] = contentValue; + } else { + delete elm.style[contentStyle]; + } + } + ); +} + +/** + * Asynchronously set focus on a content element (in content process if e10s is + * enabled, or in fission process if fission is enabled and a fission frame is + * present). + * @param {Object} browser current "tabbrowser" element + * @param {String} id content element id + * @return {Promise} promise indicating that focus is set + */ +function invokeFocus(browser, id) { + Logger.log(`Setting focus on a node with id: ${id}`); + + return invokeContentTask(browser, [id], contentId => { + const elm = content.document.getElementById(contentId); + if (elm.editor) { + elm.selectionStart = elm.selectionEnd = elm.value.length; + } + + elm.focus(); + }); +} + +/** + * Get DPR for a specific content window. + * @param browser + * Browser for which we want its content window's DPR reported. + * + * @return {Promise} + * Promise with the value that resolves to the devicePixelRatio of the + * content window of a given browser. + * + */ +function getContentDPR(browser) { + return invokeContentTask(browser, [], () => content.window.devicePixelRatio); +} + +/** + * Asynchronously perform a task in content (in content process if e10s is + * enabled, or in fission process if fission is enabled and a fission frame is + * present). + * @param {Object} browser current "tabbrowser" element + * @param {Array} args arguments for the content task + * @param {Function} task content task function + * + * @return {Promise} promise indicating that content task is complete + */ +function invokeContentTask(browser, args, task) { + return SpecialPowers.spawn( + browser, + [DEFAULT_IFRAME_ID, task.toString(), ...args], + (iframeId, contentTask, ...contentArgs) => { + // eslint-disable-next-line no-eval + const runnableTask = eval(` + (() => { + return (${contentTask}); + })();`); + const frame = content.document.getElementById(iframeId); + + return frame + ? SpecialPowers.spawn(frame, contentArgs, runnableTask) + : runnableTask.call(this, ...contentArgs); + } + ); +} + +/** + * Compare process ID's between the top level content process and possible + * remote/local iframe proccess. + * @param {Object} browser + * Top level browser object for a tab. + * @param {Boolean} isRemote + * Indicates if we expect the iframe content process to be remote or not. + */ +async function comparePIDs(browser, isRemote) { + function getProcessID() { + const { Services } = ChromeUtils.import( + "resource://gre/modules/Services.jsm" + ); + + return Services.appinfo.processID; + } + + const contentPID = await SpecialPowers.spawn(browser, [], getProcessID); + const iframePID = await invokeContentTask(browser, [], getProcessID); + is( + isRemote, + contentPID !== iframePID, + isRemote + ? "Remote IFRAME is in a different process." + : "IFRAME is in the same process." + ); +} + +/** + * Load a list of scripts into the test + * @param {Array} scripts a list of scripts to load + */ +function loadScripts(...scripts) { + for (let script of scripts) { + let path = + typeof script === "string" + ? `${CURRENT_DIR}${script}` + : `${script.dir}${script.name}`; + Services.scriptloader.loadSubScript(path, this); + } +} + +/** + * Load a list of scripts into target's content. + * @param {Object} target + * target for loading scripts into + * @param {Array} scripts + * a list of scripts to load into content + */ +async function loadContentScripts(target, ...scripts) { + for (let script of scripts) { + let contentScript; + if (typeof script === "string") { + // If script string includes a .jsm extention, assume it is a module path. + contentScript = `${CURRENT_DIR}${script}`; + } else { + // Script is a object that has { dir, name } format. + contentScript = `${script.dir}${script.name}`; + } + + let loadedScriptSet = LOADED_CONTENT_SCRIPTS.get(contentScript); + if (!loadedScriptSet) { + loadedScriptSet = new WeakSet(); + LOADED_CONTENT_SCRIPTS.set(contentScript, loadedScriptSet); + } else if (loadedScriptSet.has(target)) { + continue; + } + + await SpecialPowers.spawn(target, [contentScript], async _contentScript => { + ChromeUtils.import(_contentScript, content.window); + }); + loadedScriptSet.add(target); + } +} + +function attrsToString(attrs) { + return Object.entries(attrs) + .map(([attr, value]) => `${attr}=${JSON.stringify(value)}`) + .join(" "); +} + +function wrapWithIFrame(doc, options = {}) { + let src; + let { iframeAttrs = {}, iframeDocBodyAttrs = {} } = options; + iframeDocBodyAttrs = { + id: DEFAULT_IFRAME_DOC_BODY_ID, + ...iframeDocBodyAttrs, + }; + if (options.remoteIframe) { + const srcURL = new URL(`http://example.net/document-builder.sjs`); + if (doc.endsWith("html")) { + srcURL.searchParams.append("file", `${CURRENT_FILE_DIR}${doc}`); + } else { + srcURL.searchParams.append( + "html", + `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Fission Test</title> + </head> + <body ${attrsToString(iframeDocBodyAttrs)}>${doc}</body> + </html>` + ); + } + src = srcURL.href; + } else { + const mimeType = doc.endsWith("xhtml") ? XHTML_MIME_TYPE : HTML_MIME_TYPE; + if (doc.endsWith("html")) { + doc = loadHTMLFromFile(`${CURRENT_FILE_DIR}${doc}`); + doc = doc.replace( + /<body[.\s\S]*?>/, + `<body ${attrsToString(iframeDocBodyAttrs)}>` + ); + } else { + doc = `<body ${attrsToString(iframeDocBodyAttrs)}>${doc}</body>`; + } + + src = `data:${mimeType};charset=utf-8,${encodeURIComponent(doc)}`; + } + + iframeAttrs = { + id: DEFAULT_IFRAME_ID, + src, + ...iframeAttrs, + }; + + return `<iframe ${attrsToString(iframeAttrs)}/>`; +} + +/** + * Takes an HTML snippet or HTML doc url and returns an encoded URI for a full + * document with the snippet or the URL as a source for the IFRAME. + * @param {String} doc + * a markup snippet or url. + * @param {Object} options (see options in addAccessibleTask). + * + * @return {String} + * a base64 encoded data url of the document container the snippet. + **/ +function snippetToURL(doc, options = {}) { + const { contentDocBodyAttrs = {} } = options; + const attrs = { + id: DEFAULT_CONTENT_DOC_BODY_ID, + ...contentDocBodyAttrs, + }; + + if (gIsIframe) { + doc = wrapWithIFrame(doc, options); + } + + const encodedDoc = encodeURIComponent( + `<html> + <head> + <meta charset="utf-8"/> + <title>Accessibility Test</title> + </head> + <body ${attrsToString(attrs)}>${doc}</body> + </html>` + ); + + return `data:text/html;charset=utf-8,${encodedDoc}`; +} + +function accessibleTask(doc, task, options = {}) { + return async function() { + gIsRemoteIframe = options.remoteIframe; + gIsIframe = options.iframe || gIsRemoteIframe; + let url; + if (options.chrome && doc.endsWith("html")) { + // Load with a chrome:// URL so this loads as a chrome document in the + // parent process. + url = `${CURRENT_DIR}${doc}`; + } else if (doc.endsWith("html") && !gIsIframe) { + url = `${CURRENT_CONTENT_DIR}${doc}`; + } else { + url = snippetToURL(doc, options); + } + + registerCleanupFunction(() => { + for (let observer of Services.obs.enumerateObservers( + "accessible-event" + )) { + Services.obs.removeObserver(observer, "accessible-event"); + } + }); + + let onContentDocLoad; + if (!options.chrome) { + onContentDocLoad = waitForEvent( + EVENT_DOCUMENT_LOAD_COMPLETE, + DEFAULT_CONTENT_DOC_BODY_ID + ); + } + + let onIframeDocLoad; + if (options.remoteIframe && !options.skipFissionDocLoad) { + onIframeDocLoad = waitForEvent( + EVENT_DOCUMENT_LOAD_COMPLETE, + DEFAULT_IFRAME_DOC_BODY_ID + ); + } + + await BrowserTestUtils.withNewTab( + { + gBrowser, + url, + }, + async function(browser) { + registerCleanupFunction(() => { + if (browser) { + let tab = gBrowser.getTabForBrowser(browser); + if (tab && !tab.closing && tab.linkedBrowser) { + gBrowser.removeTab(tab); + } + } + }); + + await SimpleTest.promiseFocus(browser); + await loadContentScripts(browser, "Common.jsm"); + + if (options.chrome) { + ok(!browser.isRemoteBrowser, "Not remote browser"); + } else if (Services.appinfo.browserTabsRemoteAutostart) { + ok(browser.isRemoteBrowser, "Actually remote browser"); + } + + let docAccessible; + if (options.chrome) { + // Chrome documents don't fire DOCUMENT_LOAD_COMPLETE. Instead, wait + // until we can get the DocAccessible and it doesn't have the busy + // state. + await BrowserTestUtils.waitForCondition(() => { + docAccessible = getAccessible(browser.contentWindow.document); + if (!docAccessible) { + return false; + } + const state = {}; + docAccessible.getState(state, {}); + return !(state.value & STATE_BUSY); + }); + } else { + ({ accessible: docAccessible } = await onContentDocLoad); + } + let iframeDocAccessible; + if (gIsIframe) { + if (!options.skipFissionDocLoad) { + await comparePIDs(browser, options.remoteIframe); + iframeDocAccessible = onIframeDocLoad + ? (await onIframeDocLoad).accessible + : findAccessibleChildByID(docAccessible, DEFAULT_IFRAME_ID) + .firstChild; + } + } + + await task( + browser, + iframeDocAccessible || docAccessible, + iframeDocAccessible && docAccessible + ); + } + ); + }; +} + +/** + * A wrapper around browser test add_task that triggers an accessible test task + * as a new browser test task with given document, data URL or markup snippet. + * @param {String} doc + * URL (relative to current directory) or data URL or markup snippet + * that is used to test content with + * @param {Function|AsyncFunction} task + * a generator or a function with tests to run + * @param {null|Object} options + * Options for running accessibility test tasks: + * - {Boolean} topLevel + * Flag to run the test with content in the top level content process. + * Default is true. + * - {Boolean} chrome + * Flag to run the test with content as a chrome document in the + * parent process. Default is false. This is only valid if url is a + * relative URL to a XUL document, not a markup snippet. topLevel + * should usually be set to false in this case, since XUL documents + * don't work in the content process. + * - {Boolean} iframe + * Flag to run the test with content wrapped in an iframe. Default is + * false. + * - {Boolean} remoteIframe + * Flag to run the test with content wrapped in a remote iframe. + * Default is false. + * - {Object} iframeAttrs + * A map of attribute/value pairs to be applied to IFRAME element. + * - {Boolean} skipFissionDocLoad + * If true, the test will not wait for iframe document document + * loaded event (useful for when IFRAME is initially hidden). + * - {Object} contentDocBodyAttrs + * a set of attributes to be applied to a top level content document + * body + * - {Object} iframeDocBodyAttrs + * a set of attributes to be applied to a iframe content document body + */ +function addAccessibleTask(doc, task, options = {}) { + const { + topLevel = true, + chrome = false, + iframe = false, + remoteIframe = false, + } = options; + if (topLevel) { + add_task( + accessibleTask(doc, task, { + ...options, + chrome: false, + iframe: false, + remoteIframe: false, + }) + ); + } + + if (chrome) { + add_task( + accessibleTask(doc, task, { + ...options, + topLevel: false, + iframe: false, + remoteIframe: false, + }) + ); + } + + if (iframe) { + add_task( + accessibleTask(doc, task, { + ...options, + topLevel: false, + chrome: false, + remoteIframe: false, + }) + ); + } + + if (gFissionBrowser && remoteIframe) { + add_task( + accessibleTask(doc, task, { + ...options, + topLevel: false, + chrome: false, + iframe: false, + }) + ); + } +} + +/** + * Check if an accessible object has a defunct test. + * @param {nsIAccessible} accessible object to test defunct state for + * @return {Boolean} flag indicating defunct state + */ +function isDefunct(accessible) { + let defunct = false; + try { + let extState = {}; + accessible.getState({}, extState); + defunct = extState.value & Ci.nsIAccessibleStates.EXT_STATE_DEFUNCT; + } catch (x) { + defunct = true; + } finally { + if (defunct) { + Logger.log(`Defunct accessible: ${prettyName(accessible)}`); + } + } + return defunct; +} + +/** + * Get the DOM tag name for a given accessible. + * @param {nsIAccessible} accessible accessible + * @return {String?} tag name of associated DOM node, or null. + */ +function getAccessibleTagName(acc) { + try { + return acc.attributes.getStringProperty("tag"); + } catch (e) { + return null; + } +} + +/** + * Traverses the accessible tree starting from a given accessible as a root and + * looks for an accessible that matches based on its DOMNode id. + * @param {nsIAccessible} accessible root accessible + * @param {String} id id to look up accessible for + * @param {Array?} interfaces the interface or an array interfaces + * to query it/them from obtained accessible + * @return {nsIAccessible?} found accessible if any + */ +function findAccessibleChildByID(accessible, id, interfaces) { + if (getAccessibleDOMNodeID(accessible) === id) { + return queryInterfaces(accessible, interfaces); + } + for (let i = 0; i < accessible.children.length; ++i) { + let found = findAccessibleChildByID(accessible.getChildAt(i), id); + if (found) { + return queryInterfaces(found, interfaces); + } + } + return null; +} + +function queryInterfaces(accessible, interfaces) { + if (!interfaces) { + return accessible; + } + + for (let iface of interfaces.filter(i => !(accessible instanceof i))) { + try { + accessible.QueryInterface(iface); + } catch (e) { + ok(false, "Can't query " + iface); + } + } + + return accessible; +} + +function arrayFromChildren(accessible) { + return Array.from({ length: accessible.childCount }, (c, i) => + accessible.getChildAt(i) + ); +} + +/** + * Force garbage collection. + */ +function forceGC() { + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); +} + +/* + * This function spawns a content task and awaits expected mutation events from + * various content changes. It's good at catching events we did *not* expect. We + * do this advancing the layout refresh to flush the relocations/insertions + * queue. + */ +async function contentSpawnMutation(browser, waitFor, func, args = []) { + let onReorders = waitForEvents({ expected: waitFor.expected || [] }); + let unexpectedListener = new UnexpectedEvents(waitFor.unexpected || []); + + function tick() { + // 100ms is an arbitrary positive number to advance the clock. + // We don't need to advance the clock for a11y mutations, but other + // tick listeners may depend on an advancing clock with each refresh. + content.windowUtils.advanceTimeAndRefresh(100); + } + + // This stops the refreh driver from doing its regular ticks, and leaves + // us in control. + await invokeContentTask(browser, [], tick); + + // Perform the tree mutation. + await invokeContentTask(browser, args, func); + + // Do one tick to flush our queue (insertions, relocations, etc.) + await invokeContentTask(browser, [], tick); + + let events = await onReorders; + + unexpectedListener.stop(); + + // Go back to normal refresh driver ticks. + await invokeContentTask(browser, [], function() { + content.windowUtils.restoreNormalRefresh(); + }); + + return events; +} + +async function waitForImageMap(browser, accDoc, id = "imgmap") { + const acc = findAccessibleChildByID(accDoc, id); + if (acc.firstChild) { + return; + } + + const onReorder = waitForEvent(EVENT_REORDER, id); + // Wave over image map + await invokeContentTask(browser, [id], contentId => { + const { ContentTaskUtils } = ChromeUtils.import( + "resource://testing-common/ContentTaskUtils.jsm" + ); + const EventUtils = ContentTaskUtils.getEventUtils(content); + EventUtils.synthesizeMouse( + content.document.getElementById(contentId), + 10, + 10, + { type: "mousemove" }, + content + ); + }); + await onReorder; +} + +async function getContentBoundsForDOMElm(browser, id) { + return invokeContentTask(browser, [id], contentId => { + const { Layout: LayoutUtils } = ChromeUtils.import( + "chrome://mochitests/content/browser/accessible/tests/browser/Layout.jsm" + ); + + return LayoutUtils.getBoundsForDOMElm(contentId, content.document); + }); +} diff --git a/accessible/tests/browser/states/browser.ini b/accessible/tests/browser/states/browser.ini new file mode 100644 index 0000000000..726c7f4f03 --- /dev/null +++ b/accessible/tests/browser/states/browser.ini @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/mochitest/*.js + !/accessible/tests/browser/*.jsm + +[browser_test_link.js] +skip-if = verify +[browser_test_visibility.js] +[browser_deck_has_out_of_process_iframe.js] +skip-if = (!debug && webrender && (os == 'win')) # bug 1584037 +support-files = + target.html + test_deck_has_out_of_process_iframe.xhtml +[browser_offscreen_element_in_out_of_process_iframe.js] +skip-if = (webrender && os == 'win') # bug 1580706 diff --git a/accessible/tests/browser/states/browser_deck_has_out_of_process_iframe.js b/accessible/tests/browser/states/browser_deck_has_out_of_process_iframe.js new file mode 100644 index 0000000000..155ac4c954 --- /dev/null +++ b/accessible/tests/browser/states/browser_deck_has_out_of_process_iframe.js @@ -0,0 +1,133 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const { Preferences } = ChromeUtils.import( + "resource://gre/modules/Preferences.jsm" +); + +const DIRPATH = getRootDirectory(gTestPath).replace( + "chrome://mochitests/content/", + "" +); +const parentPATH = DIRPATH + "test_deck_has_out_of_process_iframe.xhtml"; +const iframePATH = DIRPATH + "target.html"; + +// XXX: Using external files here since using data URL breaks something, e.g. it +// makes querying the second iframe in a hidden deck failure for some reasons. +const parentURL = `http://example.com/${parentPATH}`; +const iframeURL = `http://example.org/${iframePATH}`; + +add_task(async function() { + if (Preferences.locked("fission.autostart")) { + ok( + true, + "fission.autostart pref is locked on this channel which means " + + "we don't need to run the following tests" + ); + return; + } + + const win = await BrowserTestUtils.openNewBrowserWindow({ + fission: true, + }); + + try { + const browser = win.gBrowser.selectedTab.linkedBrowser; + + BrowserTestUtils.loadURI(browser, parentURL); + await BrowserTestUtils.browserLoaded(browser, false, parentURL); + + async function setupIFrame(id, url) { + const iframe = content.document.getElementById(id); + + iframe.contentWindow.location = url; + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + + return iframe.browsingContext; + } + + async function spawnSelectDeck(index) { + async function selectDeck(i) { + const deck = content.document.getElementById("deck"); + + deck.setAttribute("selectedIndex", i); + await new Promise(resolve => { + content.window.addEventListener("MozAfterPaint", resolve, { + once: true, + }); + }); + return deck.selectedIndex; + } + await SpecialPowers.spawn(browser, [index], selectDeck); + + await waitForIFrameUpdates(); + } + + const firstIFrame = await SpecialPowers.spawn( + browser, + ["first", iframeURL], + setupIFrame + ); + const secondIFrame = await SpecialPowers.spawn( + browser, + ["second", iframeURL], + setupIFrame + ); + + await waitForIFrameUpdates(); + + await spawnTestStates( + firstIFrame, + "target", + 0, + nsIAccessibleStates.STATE_OFFSCREEN + ); + // Disable the check for the target element in the unselected pane of the + // deck, this should be fixed by bug 1578932. + // Note: As of now we can't use todo in the script transfered into the + // out-of-process. + //await spawnTestStates( + // secondIFrame, + // "target", + // nsIAccessibleStates.STATE_OFFSCREEN, + // nsIAccessibleStates.STATE_INVISIBLE + //); + + // Select the second panel. + await spawnSelectDeck(1); + await spawnTestStates( + firstIFrame, + "target", + nsIAccessibleStates.STATE_OFFSCREEN, + nsIAccessibleStates.STATE_INVISIBLE + ); + await spawnTestStates( + secondIFrame, + "target", + 0, + nsIAccessibleStates.STATE_OFFSCREEN + ); + + // Select the first panel again. + await spawnSelectDeck(0); + await spawnTestStates( + firstIFrame, + "target", + 0, + nsIAccessibleStates.STATE_OFFSCREEN + ); + await spawnTestStates( + secondIFrame, + "target", + nsIAccessibleStates.STATE_OFFSCREEN, + nsIAccessibleStates.STATE_INVISIBLE + ); + } finally { + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/accessible/tests/browser/states/browser_offscreen_element_in_out_of_process_iframe.js b/accessible/tests/browser/states/browser_offscreen_element_in_out_of_process_iframe.js new file mode 100644 index 0000000000..289d3fec9a --- /dev/null +++ b/accessible/tests/browser/states/browser_offscreen_element_in_out_of_process_iframe.js @@ -0,0 +1,98 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const parentURL = + "data:text/html;charset=utf-8," + + '<div id="scroller" style="width: 300px; height: 300px; overflow-y: scroll; overflow-x: hidden;">' + + ' <div style="width: 100%; height: 1000px;"></div>' + + ' <iframe frameborder="0"/>' + + "</div>"; +const iframeURL = + "data:text/html;charset=utf-8," + + "<style>" + + " html,body {" + + " /* Convenient for calculation of element positions */" + + " margin: 0;" + + " padding: 0;" + + " }" + + "</style>" + + '<div id="target" style="width: 100px; height: 100px;">target</div>'; + +add_task(async function() { + const win = await BrowserTestUtils.openNewBrowserWindow({ + fission: true, + }); + + try { + const browser = win.gBrowser.selectedTab.linkedBrowser; + + BrowserTestUtils.loadURI(browser, parentURL); + await BrowserTestUtils.browserLoaded(browser, false, parentURL); + + async function setup(url) { + const iframe = content.document.querySelector("iframe"); + + iframe.contentWindow.location = url; + await new Promise(resolve => { + iframe.addEventListener("load", resolve, { once: true }); + }); + + return iframe.browsingContext; + } + + async function scrollTo(x, y) { + await SpecialPowers.spawn(browser, [x, y], async (scrollX, scrollY) => { + const scroller = content.document.getElementById("scroller"); + scroller.scrollTo(scrollX, scrollY); + await new Promise(resolve => { + scroller.addEventListener("scroll", resolve, { once: true }); + }); + }); + await waitForIFrameUpdates(); + } + + // Setup an out-of-process iframe which is initially scrolled out. + const iframe = await SpecialPowers.spawn(browser, [iframeURL], setup); + + await waitForIFrameA11yReady(iframe); + await spawnTestStates( + iframe, + "target", + nsIAccessibleStates.STATE_OFFSCREEN, + nsIAccessibleStates.STATE_INVISIBLE + ); + + // Scroll the iframe into view and the target element is also visible but + // the visible area height is 11px. + await scrollTo(0, 711); + await spawnTestStates( + iframe, + "target", + nsIAccessibleStates.STATE_OFFSCREEN, + nsIAccessibleStates.STATE_INVISIBLE + ); + + // Scroll to a position where the visible height is 13px. + await scrollTo(0, 713); + await spawnTestStates( + iframe, + "target", + 0, + nsIAccessibleStates.STATE_OFFSCREEN + ); + + // Scroll the iframe out again. + await scrollTo(0, 0); + await spawnTestStates( + iframe, + "target", + nsIAccessibleStates.STATE_OFFSCREEN, + nsIAccessibleStates.STATE_INVISIBLE + ); + } finally { + await BrowserTestUtils.closeWindow(win); + } +}); diff --git a/accessible/tests/browser/states/browser_test_link.js b/accessible/tests/browser/states/browser_test_link.js new file mode 100644 index 0000000000..f747a69971 --- /dev/null +++ b/accessible/tests/browser/states/browser_test_link.js @@ -0,0 +1,47 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +async function runTests(browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + // a: no traversed state + testStates(getAcc("link_traversed"), 0, 0, STATE_TRAVERSED); + + let onStateChanged = waitForEvent(EVENT_STATE_CHANGE, "link_traversed"); + let newWinOpened = BrowserTestUtils.waitForNewWindow(); + + await BrowserTestUtils.synthesizeMouse( + "#link_traversed", + 1, + 1, + { shiftKey: true }, + browser + ); + + await onStateChanged; + testStates(getAcc("link_traversed"), STATE_TRAVERSED); + + let newWin = await newWinOpened; + await BrowserTestUtils.closeWindow(newWin); +} + +/** + * Test caching of accessible object states + */ +addAccessibleTask( + ` + <a id="link_traversed" href="http://www.example.com" target="_top"> + example.com + </a>`, + runTests +); diff --git a/accessible/tests/browser/states/browser_test_visibility.js b/accessible/tests/browser/states/browser_test_visibility.js new file mode 100644 index 0000000000..572c6f57d3 --- /dev/null +++ b/accessible/tests/browser/states/browser_test_visibility.js @@ -0,0 +1,52 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +/* import-globals-from ../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +async function runTest(browser, accDoc) { + let getAcc = id => findAccessibleChildByID(accDoc, id); + + testStates(getAcc("div"), 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + + let input = getAcc("input_scrolledoff"); + testStates(input, STATE_OFFSCREEN, 0, STATE_INVISIBLE); + + // scrolled off item (twice) + let lastLi = getAcc("li_last"); + testStates(lastLi, STATE_OFFSCREEN, 0, STATE_INVISIBLE); + + // scroll into view the item + await SpecialPowers.spawn(browser, [], () => { + content.document.getElementById("li_last").scrollIntoView(true); + }); + testStates(lastLi, 0, 0, STATE_OFFSCREEN | STATE_INVISIBLE); + + // first item is scrolled off now (testcase for bug 768786) + let firstLi = getAcc("li_first"); + testStates(firstLi, STATE_OFFSCREEN, 0, STATE_INVISIBLE); + + let newTab = await BrowserTestUtils.openNewForegroundTab(gBrowser); + // Accessibles in background tab should have offscreen state and no + // invisible state. + testStates(getAcc("div"), STATE_OFFSCREEN, 0, STATE_INVISIBLE); + BrowserTestUtils.removeTab(newTab); +} + +addAccessibleTask( + ` + <div id="div" style="border:2px solid blue; width: 500px; height: 110vh;"></div> + <input id="input_scrolledoff"> + <ul style="border:2px solid red; width: 100px; height: 50px; overflow: auto;"> + <li id="li_first">item1</li><li>item2</li><li>item3</li> + <li>item4</li><li>item5</li><li id="li_last">item6</li> + </ul>`, + runTest +); diff --git a/accessible/tests/browser/states/head.js b/accessible/tests/browser/states/head.js new file mode 100644 index 0000000000..f9d7393229 --- /dev/null +++ b/accessible/tests/browser/states/head.js @@ -0,0 +1,81 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* exported waitForIFrameA11yReady, waitForIFrameUpdates, spawnTestStates */ + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); + +// This is another version of addA11yLoadEvent for fission. +async function waitForIFrameA11yReady(iFrameBrowsingContext) { + await SimpleTest.promiseFocus(window); + + await SpecialPowers.spawn(iFrameBrowsingContext, [], () => { + return new Promise(resolve => { + function waitForDocLoad() { + SpecialPowers.executeSoon(() => { + const acc = SpecialPowers.Cc[ + "@mozilla.org/accessibilityService;1" + ].getService(SpecialPowers.Ci.nsIAccessibilityService); + + const accDoc = acc.getAccessibleFor(content.document); + let state = {}; + accDoc.getState(state, {}); + if (state.value & SpecialPowers.Ci.nsIAccessibleStates.STATE_BUSY) { + SpecialPowers.executeSoon(waitForDocLoad); + return; + } + resolve(); + }, 0); + } + waitForDocLoad(); + }); + }); +} + +// A utility function to make sure the information of scroll position or visible +// area changes reach to out-of-process iframes. +async function waitForIFrameUpdates() { + // Wait for two frames since the information is notified via asynchronous IPC + // calls. + await new Promise(resolve => requestAnimationFrame(resolve)); + await new Promise(resolve => requestAnimationFrame(resolve)); +} + +// A utility function to test the state of |elementId| element in out-of-process +// |browsingContext|. +async function spawnTestStates(browsingContext, elementId, expectedStates) { + function testStates(id, expected, unexpected) { + const acc = SpecialPowers.Cc[ + "@mozilla.org/accessibilityService;1" + ].getService(SpecialPowers.Ci.nsIAccessibilityService); + const target = content.document.getElementById(id); + let state = {}; + acc.getAccessibleFor(target).getState(state, {}); + if (expected === 0) { + Assert.equal(state.value, expected); + } else { + Assert.ok(state.value & expected); + } + Assert.ok(!(state.value & unexpected)); + } + await SpecialPowers.spawn( + browsingContext, + [elementId, expectedStates], + testStates + ); +} diff --git a/accessible/tests/browser/states/target.html b/accessible/tests/browser/states/target.html new file mode 100644 index 0000000000..0412c13d53 --- /dev/null +++ b/accessible/tests/browser/states/target.html @@ -0,0 +1,3 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<div id="target">target</div> diff --git a/accessible/tests/browser/states/test_deck_has_out_of_process_iframe.xhtml b/accessible/tests/browser/states/test_deck_has_out_of_process_iframe.xhtml new file mode 100644 index 0000000000..72d93ca30a --- /dev/null +++ b/accessible/tests/browser/states/test_deck_has_out_of_process_iframe.xhtml @@ -0,0 +1,10 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="XUL elements visibility states testing"> +<deck id="deck" selectedIndex="0"> + <iframe id="first" flex="1"/> + <iframe id="second" flex="1"/> +</deck> +</window> diff --git a/accessible/tests/browser/telemetry/browser.ini b/accessible/tests/browser/telemetry/browser.ini new file mode 100644 index 0000000000..8a6381f19f --- /dev/null +++ b/accessible/tests/browser/telemetry/browser.ini @@ -0,0 +1,3 @@ +[browser_HCM_telemetry.js] +support-files = + !/browser/components/preferences/tests/head.js diff --git a/accessible/tests/browser/telemetry/browser_HCM_telemetry.js b/accessible/tests/browser/telemetry/browser_HCM_telemetry.js new file mode 100644 index 0000000000..d9c48f5942 --- /dev/null +++ b/accessible/tests/browser/telemetry/browser_HCM_telemetry.js @@ -0,0 +1,177 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* import-globals-from ../../../../browser/components/preferences/tests/head.js */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/browser/components/preferences/tests/head.js", + this +); + +ChromeUtils.import("resource://testing-common/TelemetryTestUtils.jsm", this); + +registerCleanupFunction(() => { + Services.prefs.clearUserPref("browser.display.document_color_use"); + Services.prefs.clearUserPref("browser.display.permit_backplate"); + Services.telemetry.clearEvents(); +}); + +async function openColorsDialog() { + await openPreferencesViaOpenPreferencesAPI("general", { leaveOpen: true }); + const colorsButton = gBrowser.selectedBrowser.contentDocument.getElementById( + "colors" + ); + + const dialogOpened = promiseLoadSubDialog( + "chrome://browser/content/preferences/dialogs/colors.xhtml" + ); + colorsButton.doCommand(); + + return dialogOpened; +} + +async function closeColorsDialog(dialogWin) { + const dialogClosed = BrowserTestUtils.waitForEvent(dialogWin, "unload"); + dialogWin.document + .getElementById("ColorsDialog") + .getButton("accept") + .doCommand(); + return dialogClosed; +} + +function verifyBackplate(expectedValue) { + const snapshot = TelemetryTestUtils.getProcessScalars("parent", false, true); + ok("a11y.backplate" in snapshot, "Backplate scalar must be present."); + is( + snapshot["a11y.backplate"], + expectedValue, + "Backplate scalar is logged as " + expectedValue + ); +} + +add_task(async function testInit() { + const dialogWin = await openColorsDialog(); + const menulistHCM = dialogWin.document.getElementById("useDocumentColors"); + if (AppConstants.platform == "win") { + is( + menulistHCM.value, + "0", + "HCM menulist should be set to only with HCM theme on startup for windows" + ); + + // Verify correct default value on windows + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "a11y.theme", + "default", + false + ); + } else { + is( + menulistHCM.value, + "1", + "HCM menulist should be set to never on startup for non-windows platforms" + ); + + // Verify correct default value on non-windows + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "a11y.theme", + "always", + false + ); + } + + gBrowser.removeCurrentTab(); +}); + +add_task(async function testSetAlways() { + const dialogWin = await openColorsDialog(); + const menulistHCM = dialogWin.document.getElementById("useDocumentColors"); + + menulistHCM.doCommand(); + const newOption = dialogWin.document.getElementById("documentColorAlways"); + newOption.click(); + + is(menulistHCM.value, "2", "HCM menulist should be set to always"); + + await closeColorsDialog(dialogWin); + + // Verify correct initial value + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "a11y.theme", + "never", + false + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function testSetDefault() { + const dialogWin = await openColorsDialog(); + const menulistHCM = dialogWin.document.getElementById("useDocumentColors"); + + menulistHCM.doCommand(); + const newOption = dialogWin.document.getElementById("documentColorAutomatic"); + newOption.click(); + + is(menulistHCM.value, "0", "HCM menulist should be set to default"); + + await closeColorsDialog(dialogWin); + + // Verify correct initial value + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "a11y.theme", + "default", + false + ); + + gBrowser.removeCurrentTab(); +}); + +add_task(async function testSetNever() { + const dialogWin = await openColorsDialog(); + const menulistHCM = dialogWin.document.getElementById("useDocumentColors"); + + menulistHCM.doCommand(); + const newOption = dialogWin.document.getElementById("documentColorNever"); + newOption.click(); + + is(menulistHCM.value, "1", "HCM menulist should be set to never"); + + await closeColorsDialog(dialogWin); + + // Verify correct initial value + TelemetryTestUtils.assertKeyedScalar( + TelemetryTestUtils.getProcessScalars("parent", true, true), + "a11y.theme", + "always", + false + ); + + gBrowser.removeCurrentTab(); +}); + +// XXX We're uable to assert scalars that are false using assertScalar util, +// so we directly query the scalar in the scalar list below. (bug 1633883) + +add_task(async function testBackplate() { + is( + Services.prefs.getBoolPref("browser.display.permit_backplate"), + true, + "Backplate is init'd to true" + ); + + Services.prefs.setBoolPref("browser.display.permit_backplate", false); + // Verify correct recorded value + verifyBackplate(false); + + Services.prefs.setBoolPref("browser.display.permit_backplate", true); + // Verify correct recorded value + verifyBackplate(true); +}); diff --git a/accessible/tests/browser/tree/browser.ini b/accessible/tests/browser/tree/browser.ini new file mode 100644 index 0000000000..8072f55c37 --- /dev/null +++ b/accessible/tests/browser/tree/browser.ini @@ -0,0 +1,11 @@ +[DEFAULT] +support-files = + head.js + !/accessible/tests/browser/shared-head.js + !/accessible/tests/mochitest/*.js + !/accessible/tests/browser/*.jsm + +[browser_aria_owns.js] +skip-if = true || (verify && !debug && (os == 'linux')) #Bug 1445513 +[browser_searchbar.js] +[browser_shadowdom.js] diff --git a/accessible/tests/browser/tree/browser_aria_owns.js b/accessible/tests/browser/tree/browser_aria_owns.js new file mode 100644 index 0000000000..084ac83fea --- /dev/null +++ b/accessible/tests/browser/tree/browser_aria_owns.js @@ -0,0 +1,278 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +let NO_MOVE = { unexpected: [[EVENT_REORDER, "container"]] }; +let MOVE = { expected: [[EVENT_REORDER, "container"]] }; + +// Set last ordinal child as aria-owned, should produce no reorder. +addAccessibleTask( + `<ul id="container"><li id="a">Test</li></ul>`, + async function(browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + + testChildrenIds(containerAcc, ["a"]); + + await contentSpawnMutation(browser, NO_MOVE, function() { + // aria-own ordinal child in place, should be a no-op. + content.document + .getElementById("container") + .setAttribute("aria-owns", "a"); + }); + + testChildrenIds(containerAcc, ["a"]); + } +); + +// Add a new ordinal child to a container with an aria-owned child. +// Order should respect aria-owns. +addAccessibleTask( + `<ul id="container"><li id="a">Test</li></ul>`, + async function(browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + + testChildrenIds(containerAcc, ["a"]); + + await contentSpawnMutation(browser, MOVE, function() { + let container = content.document.getElementById("container"); + container.setAttribute("aria-owns", "a"); + + let aa = content.document.createElement("li"); + aa.id = "aa"; + container.appendChild(aa); + }); + + testChildrenIds(containerAcc, ["aa", "a"]); + + await contentSpawnMutation(browser, MOVE, function() { + content.document.getElementById("container").removeAttribute("aria-owns"); + }); + + testChildrenIds(containerAcc, ["a", "aa"]); + } +); + +// Remove a no-move aria-owns attribute, should result in a no-move. +addAccessibleTask( + `<ul id="container" aria-owns="a"><li id="a">Test</li></ul>`, + async function(browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + + testChildrenIds(containerAcc, ["a"]); + + await contentSpawnMutation(browser, NO_MOVE, function() { + // remove aria-owned child that is already ordinal, should be no-op. + content.document.getElementById("container").removeAttribute("aria-owns"); + }); + + testChildrenIds(containerAcc, ["a"]); + } +); + +// Attempt to steal an aria-owned child. The attempt should fail. +addAccessibleTask( + ` + <ul> + <li id="a">Test</li> + </ul> + <ul aria-owns="a"></ul> + <ul id="container"></ul>`, + async function(browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + + testChildrenIds(containerAcc, []); + + await contentSpawnMutation(browser, NO_MOVE, function() { + content.document + .getElementById("container") + .setAttribute("aria-owns", "a"); + }); + + testChildrenIds(containerAcc, []); + } +); + +// Don't aria-own children of <select> +addAccessibleTask( + ` + <div id="container" role="group" aria-owns="b"></div> + <select id="select"> + <option id="a"></option> + <option id="b"></option> + </select>`, + async function(browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + let selectAcc = findAccessibleChildByID(accDoc, "select"); + + testChildrenIds(containerAcc, []); + testChildrenIds(selectAcc.firstChild, ["a", "b"]); + } +); + +// Don't allow <select> to aria-own +addAccessibleTask( + ` + <div id="container" role="group"> + <div id="a"></div> + <div id="b"></div> + </div> + <select id="select" aria-owns="a"> + <option id="c"></option> + </select>`, + async function(browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + let selectAcc = findAccessibleChildByID(accDoc, "select"); + + testChildrenIds(containerAcc, ["a", "b"]); + testChildrenIds(selectAcc.firstChild, ["c"]); + } +); + +// Don't allow one <select> to aria-own an <option> from another <select>. +addAccessibleTask( + ` + <select id="select1" aria-owns="c"> + <option id="a"></option> + <option id="b"></option> + </select> + <select id="select2"> + <option id="c"></option> + </select>`, + async function(browser, accDoc) { + let selectAcc1 = findAccessibleChildByID(accDoc, "select1"); + let selectAcc2 = findAccessibleChildByID(accDoc, "select2"); + + testChildrenIds(selectAcc1.firstChild, ["a", "b"]); + testChildrenIds(selectAcc2.firstChild, ["c"]); + } +); + +// Don't allow a <select> to reorder its children with aria-owns. +addAccessibleTask( + ` + <select id="container" aria-owns="c b a"> + <option id="a"></option> + <option id="b"></option> + <option id="c"></option> + </select>`, + async function(browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + + testChildrenIds(containerAcc.firstChild, ["a", "b", "c"]); + + await contentSpawnMutation(browser, NO_MOVE, function() { + content.document + .getElementById("container") + .setAttribute("aria-owns", "a c b"); + }); + + testChildrenIds(containerAcc.firstChild, ["a", "b", "c"]); + } +); + +// Don't crash if ID in aria-owns does not exist +addAccessibleTask( + ` + <select id="container" aria-owns="boom" multiple></select>`, + async function(browser, accDoc) { + ok(true, "Did not crash"); + } +); + +addAccessibleTask( + ` + <ul id="one"> + <li id="a">Test</li> + <li id="b">Test 2</li> + <li id="c">Test 3</li> + </ul> + <ul id="two"></ul>`, + async function(browser, accDoc) { + let one = findAccessibleChildByID(accDoc, "one"); + let two = findAccessibleChildByID(accDoc, "two"); + + let waitfor = { + expected: [ + [EVENT_REORDER, "one"], + [EVENT_REORDER, "two"], + ], + }; + + await contentSpawnMutation(browser, waitfor, function() { + // Put same id twice in aria-owns + content.document.getElementById("two").setAttribute("aria-owns", "a a"); + }); + + testChildrenIds(one, ["b", "c"]); + testChildrenIds(two, ["a"]); + + await contentSpawnMutation(browser, waitfor, function() { + // If the previous double-id aria-owns worked correctly, we should + // be in a good state and all is fine.. + content.document.getElementById("two").setAttribute("aria-owns", "a b"); + }); + + testChildrenIds(one, ["c"]); + testChildrenIds(two, ["a", "b"]); + } +); + +addAccessibleTask(`<div id="a"></div><div id="b"></div>`, async function( + browser, + accDoc +) { + testChildrenIds(accDoc, ["a", "b"]); + + let waitFor = { + expected: [[EVENT_REORDER, e => e.accessible == accDoc]], + }; + + await contentSpawnMutation(browser, waitFor, function() { + content.document.documentElement.style.display = "none"; + content.document.documentElement.getBoundingClientRect(); + content.document.body.setAttribute("aria-owns", "b a"); + content.document.documentElement.remove(); + }); + + testChildrenIds(accDoc, []); +}); + +// Don't allow ordinal child to be placed after aria-owned child (bug 1405796) +addAccessibleTask( + `<div id="container"><div id="a">Hello</div></div> + <div><div id="c">There</div><div id="d">There</div></div>`, + async function(browser, accDoc) { + let containerAcc = findAccessibleChildByID(accDoc, "container"); + + testChildrenIds(containerAcc, ["a"]); + + await contentSpawnMutation(browser, MOVE, function() { + content.document + .getElementById("container") + .setAttribute("aria-owns", "c"); + }); + + testChildrenIds(containerAcc, ["a", "c"]); + + await contentSpawnMutation(browser, MOVE, function() { + let span = content.document.createElement("span"); + content.document.getElementById("container").appendChild(span); + + let b = content.document.createElement("div"); + b.id = "b"; + content.document.getElementById("container").appendChild(b); + }); + + testChildrenIds(containerAcc, ["a", "b", "c"]); + + await contentSpawnMutation(browser, MOVE, function() { + content.document + .getElementById("container") + .setAttribute("aria-owns", "c d"); + }); + + testChildrenIds(containerAcc, ["a", "b", "c", "d"]); + } +); diff --git a/accessible/tests/browser/tree/browser_searchbar.js b/accessible/tests/browser/tree/browser_searchbar.js new file mode 100644 index 0000000000..e41ba9819d --- /dev/null +++ b/accessible/tests/browser/tree/browser_searchbar.js @@ -0,0 +1,52 @@ +"use strict"; + +/* import-globals-from ../../mochitest/role.js */ +loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); + +// eslint-disable-next-line camelcase +add_task(async function test_searchbar_a11y_tree() { + await SpecialPowers.pushPrefEnv({ + set: [["browser.search.widget.inNavBar", true]], + }); + + let searchbar = await TestUtils.waitForCondition( + () => document.getElementById("searchbar"), + "wait for search bar to appear" + ); + + // Make sure the popup has been rendered so it shows up in the a11y tree. + let popup = document.getElementById("PopupSearchAutoComplete"); + let promise = BrowserTestUtils.waitForEvent(popup, "popupshown", false); + searchbar.textbox.openPopup(); + await promise; + + promise = BrowserTestUtils.waitForEvent(popup, "popuphidden", false); + searchbar.textbox.closePopup(); + await promise; + + const TREE = { + role: ROLE_EDITCOMBOBOX, + + children: [ + // input element + { + role: ROLE_ENTRY, + children: [], + }, + + // context menu + { + role: ROLE_COMBOBOX_LIST, + children: [], + }, + + // result list + { + role: ROLE_GROUPING, + // not testing the structure inside the result list + }, + ], + }; + + testAccessibleTree(searchbar, TREE); +}); diff --git a/accessible/tests/browser/tree/browser_shadowdom.js b/accessible/tests/browser/tree/browser_shadowdom.js new file mode 100644 index 0000000000..728284d3e2 --- /dev/null +++ b/accessible/tests/browser/tree/browser_shadowdom.js @@ -0,0 +1,64 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +const REORDER = { expected: [[EVENT_REORDER, "container"]] }; + +// Dynamically inserted slotted accessible elements should be in +// the accessible tree. +const snippet = ` +<script> +customElements.define("x-el", class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.innerHTML = + "<div role='presentation'><slot></slot></div>"; + } +}); +</script> +<x-el id="container" role="group"><label id="l1">label1</label></x-el> +`; + +addAccessibleTask(snippet, async function(browser, accDoc) { + let container = findAccessibleChildByID(accDoc, "container"); + + testChildrenIds(container, ["l1"]); + + await contentSpawnMutation(browser, REORDER, function() { + let labelEl = content.document.createElement("label"); + labelEl.id = "l2"; + + let containerEl = content.document.getElementById("container"); + containerEl.appendChild(labelEl); + }); + + testChildrenIds(container, ["l1", "l2"]); +}); + +// Dynamically inserted not accessible custom element containing an accessible +// in its shadow DOM. +const snippet2 = ` +<script> +customElements.define("x-el2", class extends HTMLElement { + constructor() { + super(); + this.attachShadow({ mode: "open" }); + this.shadowRoot.innerHTML = "<input id='input'>"; + } +}); +</script> +<div role="group" id="container"></div> +`; + +addAccessibleTask(snippet2, async function(browser, accDoc) { + let container = findAccessibleChildByID(accDoc, "container"); + + await contentSpawnMutation(browser, REORDER, function() { + content.document.getElementById("container").innerHTML = "<x-el2></x-el2>"; + }); + + testChildrenIds(container, ["input"]); +}); diff --git a/accessible/tests/browser/tree/head.js b/accessible/tests/browser/tree/head.js new file mode 100644 index 0000000000..867a1b1417 --- /dev/null +++ b/accessible/tests/browser/tree/head.js @@ -0,0 +1,34 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +"use strict"; + +/* exported testChildrenIds */ + +// Load the shared-head file first. +/* import-globals-from ../shared-head.js */ +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", + this +); + +// Loading and common.js from accessible/tests/mochitest/ for all tests, as +// well as promisified-events.js. +loadScripts( + { name: "common.js", dir: MOCHITESTS_DIR }, + { name: "promisified-events.js", dir: MOCHITESTS_DIR } +); + +/* + * A test function for comparing the IDs of an accessible's children + * with an expected array of IDs. + */ +function testChildrenIds(acc, expectedIds) { + let ids = arrayFromChildren(acc).map(child => getAccessibleDOMNodeID(child)); + Assert.deepEqual( + ids, + expectedIds, + `Children for ${getAccessibleDOMNodeID(acc)} are wrong.` + ); +} |