diff options
Diffstat (limited to 'accessible/tests')
555 files changed, 81920 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.` + ); +} diff --git a/accessible/tests/crashtests/.eslintrc.js b/accessible/tests/crashtests/.eslintrc.js new file mode 100644 index 0000000000..1a103a8d38 --- /dev/null +++ b/accessible/tests/crashtests/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "plugin:mozilla/mochitest-test" + ], +}; diff --git a/accessible/tests/crashtests/1072792.xhtml b/accessible/tests/crashtests/1072792.xhtml new file mode 100644 index 0000000000..f99c64c5d3 --- /dev/null +++ b/accessible/tests/crashtests/1072792.xhtml @@ -0,0 +1,5 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<body onload="document.body.appendChild(x);"> +<table><tbody><div id="x"></div></tbody></table> +</body> +</html> diff --git a/accessible/tests/crashtests/1380199.html b/accessible/tests/crashtests/1380199.html new file mode 100644 index 0000000000..c690597173 --- /dev/null +++ b/accessible/tests/crashtests/1380199.html @@ -0,0 +1,15 @@ +<script> +window.onload=function(){ + let o=document.getElementById('a'); + let n=document.createElement('plaintext'); + o.parentNode.replaceChild(n,o); + n.setAttribute('id','a'); + document.getElementById('a').setAttribute('aria-owns','c b'); + document.getElementById('d').setAttribute('width',1); +} +</script> +> +<script id='a'></script> +<hgroup id='b'> +<h6 id='c'> +<video id='d' controls width='169' height='187'> diff --git a/accessible/tests/crashtests/1402999.html b/accessible/tests/crashtests/1402999.html new file mode 100644 index 0000000000..567f20b2df --- /dev/null +++ b/accessible/tests/crashtests/1402999.html @@ -0,0 +1,11 @@ +<script> +document.addEventListener("DOMContentLoaded", function(){ + document.getElementById('a').click(); + window.frames[0].document.body.appendChild(document.getElementById('b')); +}); +</script> +<cite id='b'> +<label> +<input/> +<strong id='a'></strong> +<iframe> diff --git a/accessible/tests/crashtests/1415667.html b/accessible/tests/crashtests/1415667.html new file mode 100644 index 0000000000..174099837d --- /dev/null +++ b/accessible/tests/crashtests/1415667.html @@ -0,0 +1 @@ +<iframe role="row"> diff --git a/accessible/tests/crashtests/1463962.html b/accessible/tests/crashtests/1463962.html new file mode 100644 index 0000000000..c3d233af5c --- /dev/null +++ b/accessible/tests/crashtests/1463962.html @@ -0,0 +1,4 @@ +<style> +#a { display: contents } +</style> +<li id="a"> diff --git a/accessible/tests/crashtests/1484778.html b/accessible/tests/crashtests/1484778.html new file mode 100644 index 0000000000..9d3fc33f59 --- /dev/null +++ b/accessible/tests/crashtests/1484778.html @@ -0,0 +1,26 @@ +<style> +#a { border-left: solid -moz-hyperlinktext 93em } +</style> +<script> +/* + I dont't know why but this seems to be required to trigger the crash... + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa + aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa +*/ +function go() { + var b = document.elementFromPoint(0,0); + window.scroll({left: 97, top: -1}); + document.adoptNode(b); +} +</script> +<body onload=go()> +<ins id="a"> diff --git a/accessible/tests/crashtests/1494707.html b/accessible/tests/crashtests/1494707.html new file mode 100644 index 0000000000..f2feb1572d --- /dev/null +++ b/accessible/tests/crashtests/1494707.html @@ -0,0 +1,15 @@ +<html>
+ <head>
+ <style>
+ *::after {
+ content: counters(bar, '', none);
+ }
+ </style>
+ </head>
+ <body>
+ <table>
+ <thead></thead>
+ <th></th>
+ </table>
+ </body>
+</html>
diff --git a/accessible/tests/crashtests/1503964.html b/accessible/tests/crashtests/1503964.html new file mode 100644 index 0000000000..bc4c359b52 --- /dev/null +++ b/accessible/tests/crashtests/1503964.html @@ -0,0 +1,4 @@ +<table>
+<th>X</th>
+<tfoot style="display: table-row">
+<tr role="columnheader">X</tr>
diff --git a/accessible/tests/crashtests/1572811.html b/accessible/tests/crashtests/1572811.html new file mode 100644 index 0000000000..61dc78ca7b --- /dev/null +++ b/accessible/tests/crashtests/1572811.html @@ -0,0 +1,9 @@ +<script>
+function go() {
+ a.style.overflow = "auto";
+}
+</script>
+<body onload=go()>
+<table id="a" background="3">
+<th>
+<textarea style="position:absolute">A</textarea>
diff --git a/accessible/tests/crashtests/1578282.html b/accessible/tests/crashtests/1578282.html new file mode 100644 index 0000000000..31cc1ce02b --- /dev/null +++ b/accessible/tests/crashtests/1578282.html @@ -0,0 +1,21 @@ +<html class="reftest-wait"> +<head> +<script> +function onload() { + document.documentElement.style.display = "none" + document.documentElement.getBoundingClientRect() + document.documentElement.style.display = "" + window.setTimeout(() => { + document.documentElement.removeAttribute("class"); + }, 1000); +} +</script> +</head> +<body onload="onload()"> +<marquee style="display: inline-grid"> +<video></video> +<bgsound></bgsound> +< +</marquee> +</body> +</html>
\ No newline at end of file diff --git a/accessible/tests/crashtests/1585851.html b/accessible/tests/crashtests/1585851.html new file mode 100644 index 0000000000..3acac2de7d --- /dev/null +++ b/accessible/tests/crashtests/1585851.html @@ -0,0 +1,21 @@ +<style>
+* {
+ border-collapse: collapse;
+}
+</style>
+<script>
+window.onload = () => {
+ c.style.setProperty("border-collapse", "separate")
+}
+function eh() {
+ a.options.add(f)
+ a.add(b, 9)
+ a.add(f, d)
+}
+</script>
+<select id="a"></select>
+<option id="b">
+<dd id="c">
+<audio id="d" controls>
+<details open="true" ontoggle="eh()">
+<optgroup id="f">
diff --git a/accessible/tests/crashtests/1655983.html b/accessible/tests/crashtests/1655983.html new file mode 100644 index 0000000000..88fee3ec5d --- /dev/null +++ b/accessible/tests/crashtests/1655983.html @@ -0,0 +1,6 @@ +<script> + document.addEventListener('DOMContentLoaded', () => { + document.getElementById('id_0').type = '' + }) +</script> +<input id="id_0" type="time"> diff --git a/accessible/tests/crashtests/448064.xhtml b/accessible/tests/crashtests/448064.xhtml new file mode 100644 index 0000000000..c1a25ca550 --- /dev/null +++ b/accessible/tests/crashtests/448064.xhtml @@ -0,0 +1,70 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> +</head> +<body> +<div id="mw_b"> +<div id="mw_f"> +<div id="mw_g" style="display: none;"/> +</div> +</div> + +<div id="mw_c" style="display: none;"> +<div id="mw_d"> +<div id="mw_e"></div> +</div> +</div> + +<input id="mw_a"/> + + +<script> +function dumpAccessibleNode(aNode, level) { + var msg = ""; + + try { + msg += "name=\"" + aNode.name + "\" "; + } catch (e) { + msg += " noName "; + } + + dump(msg + "\n"); +} + + +function dumpAccessibleTree(aNode, level) { + level = level || 0; + + dumpAccessibleNode(aNode, level); + try { + var child = aNode.firstChild; + while (child) { + dumpAccessibleTree(child, level + 1); + child = child.nextSibling; + } + } catch (e) { + dump("Error visiting child nodes: " + e + "\n"); + } +} + +function A(o) { + var acc = SpecialPowers.Cc["@mozilla.org/accessibilityService;1"] + .getService(SpecialPowers.Ci.nsIAccessibilityService); + return acc.getAccessibleFor(o); +} + +function beginAccessible() { + dumpAccessibleTree(A(document), 0); +} +setTimeout(beginAccessible, 100); + + +setTimeout(doe, 200); +function doe() { + document.getElementById("mw_a").appendChild(document.getElementById("mw_b")); + document.getElementById("mw_c").appendChild(document.getElementById("mw_d")); + document.getElementById("mw_e").appendChild(document.getElementById("mw_f")); + document.getElementById("mw_g").appendChild(document.getElementById("mw_b")); +} +</script> +</body> +</html> diff --git a/accessible/tests/crashtests/471493.xhtml b/accessible/tests/crashtests/471493.xhtml new file mode 100644 index 0000000000..ca703f2aae --- /dev/null +++ b/accessible/tests/crashtests/471493.xhtml @@ -0,0 +1,55 @@ +<?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" + title="bug 471493 'crash [@ nsPropertyTable::GetPropertyInternal]'" + onload="doTest();" class="reftest-wait"> + + <script type="application/javascript"> + <![CDATA[ + function addA11yLoadEvent(accService, func) { + function waitForDocLoad() { + setTimeout(() => { + let accDoc = accService.getAccessibleFor(document); + let state = {}; + accDoc.getState(state, {}); + + if (state.value & SpecialPowers.Ci.nsIAccessibleStates.STATE_BUSY) { + return waitForDocLoad(); + } + setTimeout(func, 0); + }, 0); + } + + waitForDocLoad(); + } + + function doTest() + { + var accService = SpecialPowers.Cc["@mozilla.org/accessibilityService;1"]. + getService(SpecialPowers.Ci.nsIAccessibilityService); + + var treecol = document.getElementById("col"); + var x = treecol.screenX; + var y = treecol.screenY; + + var tree = document.getElementById("tree"); + addA11yLoadEvent(accService, () => { + var treeAcc = accService.getAccessibleFor(tree); + treeAcc.getChildAtPoint(x + 1, y + 1); + document.documentElement.removeAttribute("class"); + }); + } + ]]> + </script> + + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + <treecol id="scol" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + +</window> + diff --git a/accessible/tests/crashtests/884202.html b/accessible/tests/crashtests/884202.html new file mode 100644 index 0000000000..f29ef55f4a --- /dev/null +++ b/accessible/tests/crashtests/884202.html @@ -0,0 +1,21 @@ +<!DOCTYPE html> +<html> +<head> +<script> +function f() +{ + window.removeEventListener("pagehide", f, false); + var root = document.documentElement; + document.removeChild(root); + document.appendChild(root); +} + +function boom() +{ + window.addEventListener("pagehide", f, false); + window.location = "data:text/html,4"; +} +</script> +</head> +<body onload="boom();"></body> +</html> diff --git a/accessible/tests/crashtests/890760.html b/accessible/tests/crashtests/890760.html new file mode 100644 index 0000000000..ecc76160b9 --- /dev/null +++ b/accessible/tests/crashtests/890760.html @@ -0,0 +1,14 @@ +<!DOCTYPE html> +<html> +<head> +<script> + +function boom() +{ + window.getSelection().collapse(document.createTextNode("."), 0); +} + +</script> +</head> +<body onload="setTimeout(boom, 0);"></body> +</html> diff --git a/accessible/tests/crashtests/893515.html b/accessible/tests/crashtests/893515.html new file mode 100644 index 0000000000..a9cc12c928 --- /dev/null +++ b/accessible/tests/crashtests/893515.html @@ -0,0 +1,15 @@ +<!DOCTYPE html> +<html> +<head> +<meta charset="UTF-8"> +<script> +function boom() +{ + document.designMode = 'on'; + var otherDoc = document.implementation.createDocument("", "", null); + otherDoc.adoptNode(document.getElementById("t")); +} +</script> +</head> +<body onload="setTimeout(boom, 0);"><textarea id="t"></textarea></body> +</html> diff --git a/accessible/tests/crashtests/crashtests.list b/accessible/tests/crashtests/crashtests.list new file mode 100644 index 0000000000..c29365cef3 --- /dev/null +++ b/accessible/tests/crashtests/crashtests.list @@ -0,0 +1,21 @@ +load 448064.xhtml # This test instantiates a11y, so be careful about adding tests before it +load chrome://reftest/content/crashtests/accessible/tests/crashtests/471493.xhtml +asserts-if(!browserIsRemote,2) load 884202.html +load 1572811.html +load 1578282.html +load 890760.html +load 893515.html +load 1072792.xhtml +load 1380199.html +load 1402999.html +load 1463962.html +load 1484778.html +load 1494707.html +load 1503964.html +load 1415667.html +load 1585851.html +load 1655983.html + +# last_test_to_unload_testsuite.xhtml MUST be the last test in the list because it +# is responsible for shutting down accessibility service affecting later tests. +skip-if(/^Windows\x20NT\x2010\.0/.test(http.oscpu)&&/^aarch64-msvc/.test(xulRuntime.XPCOMABI)) load chrome://reftest/content/crashtests/accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml diff --git a/accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml b/accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml new file mode 100644 index 0000000000..9a70c41dcf --- /dev/null +++ b/accessible/tests/crashtests/last_test_to_unload_testsuite.xhtml @@ -0,0 +1,36 @@ +<?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" + title="bug 471493 'crash [@ nsPropertyTable::GetPropertyInternal]'" + onload="shutdown();" class="reftest-wait"> + + <script type="application/javascript"> + <![CDATA[ + function shutdown() { + !SpecialPowers.Services.appinfo.accessibilityEnabled && + dump("### Error : Accessibility is expected to be enabled.\n"); + + // Force garbage collection. We try really hard to garbage collect + // everythin here to ensure that all a11y xpcom bits are shut down and + // avoid intermittent timeouts. + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); + SpecialPowers.gc(); + SpecialPowers.forceShrinkingGC(); + SpecialPowers.forceCC(); + + if (SpecialPowers.Services.appinfo.accessibilityEnabled) { + let observe = (subject, topic, data) => { + SpecialPowers.Services.obs.removeObserver(observe, "a11y-init-or-shutdown"); + data === "0" && document.documentElement.removeAttribute("class"); + }; + SpecialPowers.Services.obs.addObserver(observe, "a11y-init-or-shutdown"); + } else { + document.documentElement.removeAttribute("class"); + } + } + ]]> + </script> +</window> diff --git a/accessible/tests/mochitest/.eslintrc.js b/accessible/tests/mochitest/.eslintrc.js new file mode 100644 index 0000000000..2ce1c5017a --- /dev/null +++ b/accessible/tests/mochitest/.eslintrc.js @@ -0,0 +1,24 @@ +"use strict"; + +module.exports = { + rules: { + // XXX These are rules that are enabled in the recommended configuration, but + // disabled here due to failures when initially implemented. They should be + // removed (and hence enabled) at some stage. + "no-nested-ternary": "off", + }, + + overrides: [ + { + files: [ + // Bug 1602061 TODO: These tests access DOM elements via + // id-as-variable-name, which eslint doesn't have support for yet. + "attributes/test_listbox.html", + "treeupdate/test_ariaowns.html", + ], + rules: { + "no-undef": "off", + }, + }, + ], +}; diff --git a/accessible/tests/mochitest/a11y.ini b/accessible/tests/mochitest/a11y.ini new file mode 100644 index 0000000000..d9430e1f86 --- /dev/null +++ b/accessible/tests/mochitest/a11y.ini @@ -0,0 +1,18 @@ +[DEFAULT] +support-files = + ../../../dom/media/test/bug461281.ogg + ../../../dom/security/test/csp/dummy.pdf + dumbfile.zip + formimage.png + letters.gif + moz.png + longdesc_src.html + *.js + treeview.css + +[test_aria_token_attrs.html] +[test_bug420863.html] +[test_descr.html] +[test_nsIAccessibleDocument.html] +[test_nsIAccessibleImage.html] +[test_OuterDocAccessible.html] diff --git a/accessible/tests/mochitest/actions.js b/accessible/tests/mochitest/actions.js new file mode 100644 index 0000000000..57fe09e1c4 --- /dev/null +++ b/accessible/tests/mochitest/actions.js @@ -0,0 +1,230 @@ +/* import-globals-from common.js */ +/* import-globals-from events.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Event constants + +const MOUSEDOWN_EVENT = 1; +const MOUSEUP_EVENT = 2; +const CLICK_EVENT = 4; +const COMMAND_EVENT = 8; +const FOCUS_EVENT = 16; + +const CLICK_EVENTS = MOUSEDOWN_EVENT | MOUSEUP_EVENT | CLICK_EVENT; +const XUL_EVENTS = CLICK_EVENTS | COMMAND_EVENT; + +// ////////////////////////////////////////////////////////////////////////////// +// Public functions + +/** + * Test default accessible actions. + * + * Action tester interface is: + * + * var actionObj = { + * // identifier of accessible to perform an action on + * get ID() {}, + * + * // index of the action + * get actionIndex() {}, + * + * // name of the action + * get actionName() {}, + * + * // DOM events (see constants defined above) + * get events() {}, + * + * // [optional] identifier of target DOM events listeners are registered on, + * // used with 'events', if missing then 'ID' is used instead. + * get targetID() {}, + * + * // [optional] true to match DOM events bubbled up to the target, + * // false (default) to only match events fired directly on the target. + * get allowBubbling() {}, + * + * // [optional] perform checks when 'click' event is handled if 'events' + * // is used. + * checkOnClickEvent: function() {}, + * + * // [optional] an array of invoker's checker objects (see eventQueue + * // constructor events.js) + * get eventSeq() {} + * }; + * + * + * @param aArray [in] an array of action cheker objects + */ +function testActions(aArray) { + gActionsQueue = new eventQueue(); + + for (var idx = 0; idx < aArray.length; idx++) { + var actionObj = aArray[idx]; + var accOrElmOrID = actionObj.ID; + var actionIndex = actionObj.actionIndex; + var actionName = actionObj.actionName; + var events = actionObj.events; + var accOrElmOrIDOfTarget = actionObj.targetID + ? actionObj.targetID + : accOrElmOrID; + + var eventSeq = []; + if (events) { + var elm = getNode(accOrElmOrIDOfTarget); + if (events & MOUSEDOWN_EVENT) { + eventSeq.push(new checkerOfActionInvoker("mousedown", elm, actionObj)); + } + + if (events & MOUSEUP_EVENT) { + eventSeq.push(new checkerOfActionInvoker("mouseup", elm, actionObj)); + } + + if (events & CLICK_EVENT) { + eventSeq.push(new checkerOfActionInvoker("click", elm, actionObj)); + } + + if (events & COMMAND_EVENT) { + eventSeq.push(new checkerOfActionInvoker("command", elm, actionObj)); + } + + if (events & FOCUS_EVENT) { + eventSeq.push(new focusChecker(elm)); + } + } + + if (actionObj.eventSeq) { + eventSeq = eventSeq.concat(actionObj.eventSeq); + } + + var invoker = new actionInvoker( + accOrElmOrID, + actionIndex, + actionName, + eventSeq + ); + gActionsQueue.push(invoker); + } + + gActionsQueue.invoke(); +} + +/** + * Test action names and descriptions. + */ +function testActionNames(aID, aActions) { + var actions = typeof aActions == "string" ? [aActions] : aActions || []; + + var acc = getAccessible(aID); + is(acc.actionCount, actions.length, "Wong number of actions."); + for (var i = 0; i < actions.length; i++) { + is( + acc.getActionName(i), + actions[i], + "Wrong action name at " + i + " index." + ); + is( + acc.getActionDescription(0), + gActionDescrMap[actions[i]], + "Wrong action description at " + i + "index." + ); + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private + +var gActionsQueue = null; + +function actionInvoker(aAccOrElmOrId, aActionIndex, aActionName, aEventSeq) { + this.invoke = function actionInvoker_invoke() { + var acc = getAccessible(aAccOrElmOrId); + if (!acc) { + return INVOKER_ACTION_FAILED; + } + + var isThereActions = acc.actionCount > 0; + ok( + isThereActions, + "No actions on the accessible for " + prettyName(aAccOrElmOrId) + ); + + if (!isThereActions) { + return INVOKER_ACTION_FAILED; + } + + is( + acc.getActionName(aActionIndex), + aActionName, + "Wrong action name of the accessible for " + prettyName(aAccOrElmOrId) + ); + + try { + acc.doAction(aActionIndex); + } catch (e) { + ok(false, "doAction(" + aActionIndex + ") failed with: " + e.name); + return INVOKER_ACTION_FAILED; + } + return null; + }; + + this.eventSeq = aEventSeq; + + this.getID = function actionInvoker_getID() { + return ( + "invoke an action " + + aActionName + + " at index " + + aActionIndex + + " on " + + prettyName(aAccOrElmOrId) + ); + }; +} + +function checkerOfActionInvoker(aType, aTarget, aActionObj) { + this.type = aType; + + this.target = aTarget; + + if (aActionObj && "eventTarget" in aActionObj) { + this.eventTarget = aActionObj.eventTarget; + } + + if (aActionObj && aActionObj.allowBubbling) { + // Normally, we add event listeners on the document. To catch bubbled + // events, we need to add the listener on the target itself. + this.eventTarget = "element"; + // Normally, we only match an event fired directly on the target. Override + // this to match a bubbled event. + this.match = function(aEvent) { + return aEvent.currentTarget == aTarget; + }; + } + + this.phase = false; + + this.getID = function getID() { + return aType + " event handling"; + }; + + this.check = function check(aEvent) { + if (aType == "click" && aActionObj && "checkOnClickEvent" in aActionObj) { + aActionObj.checkOnClickEvent(aEvent); + } + }; +} + +var gActionDescrMap = { + jump: "Jump", + press: "Press", + check: "Check", + uncheck: "Uncheck", + select: "Select", + open: "Open", + close: "Close", + switch: "Switch", + click: "Click", + collapse: "Collapse", + expand: "Expand", + activate: "Activate", + cycle: "Cycle", +}; diff --git a/accessible/tests/mochitest/actions/a11y.ini b/accessible/tests/mochitest/actions/a11y.ini new file mode 100644 index 0000000000..5669e8d963 --- /dev/null +++ b/accessible/tests/mochitest/actions/a11y.ini @@ -0,0 +1,17 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + !/dom/media/test/bug461281.ogg + +[test_anchors.html] +[test_aria.html] +[test_controls.html] +[test_general.html] +[test_general.xhtml] +[test_keys.html] +[test_keys.xhtml] +[test_link.html] +[test_media.html] +[test_select.html] +[test_tree.xhtml] +[test_treegrid.xhtml] diff --git a/accessible/tests/mochitest/actions/test_anchors.html b/accessible/tests/mochitest/actions/test_anchors.html new file mode 100644 index 0000000000..6ee1e0c450 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_anchors.html @@ -0,0 +1,146 @@ +<html> + +<head> + <title>nsIAccessible actions testing for HTML links that + scroll the page to named anchors</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Event checkers + + function scrollingChecker(aAcc) { + this.type = EVENT_SCROLLING_START; + this.target = aAcc; + this.getID = function scrollingChecker_getID() { + return "scrolling start handling for " + prettyName(aAcc); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; // debug stuff + // gA11yEventDumpToConsole = true; // debug stuff + + function doTest() { + var actionsArray = [ + { + ID: "anchor1", + actionName: "jump", + actionIndex: 0, + events: CLICK_EVENTS, + eventSeq: [ + new scrollingChecker(getAccessible("bottom1")), + ], + }, + { // jump again (test for bug 437607) + ID: "anchor1", + actionName: "jump", + actionIndex: 0, + events: CLICK_EVENTS, + eventSeq: [ + new scrollingChecker(getAccessible("bottom1")), + ], + }, + { + ID: "anchor2", + actionName: "jump", + actionIndex: 0, + events: CLICK_EVENTS, + eventSeq: [ + new scrollingChecker(getAccessible("bottom2")), + ], + }, + ]; + + testActions(actionsArray); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=506389" + title="Some same page links do not fire EVENT_SYSTEM_SCROLLINGSTART"> + Mozilla Bug 506389 + </a><br> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=437607" + title="Clicking the 'Skip to main content' link once works, second time fails to initiate a V cursor jump"> + Mozilla Bug 437607 + </a><br> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=519303" + title="Same page links to targets with content fires scrolling start accessible event on leaf text node"> + Mozilla Bug 519303 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="debug"></div> + + <h1>This is a test page for anchors</h1> + This is a top anchor<a name="Top"> + </a><a id="anchor1" href="#bottom1">Link to anchor</a> + <a id="anchor2" href="#bottom2">Link to div</a> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br>This is some text in the middle<br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + This is some text. + This is a bottom anchor<a id="bottom1"></a> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <div id="bottom2">This is a div</div> +</body> +</html> diff --git a/accessible/tests/mochitest/actions/test_aria.html b/accessible/tests/mochitest/actions/test_aria.html new file mode 100644 index 0000000000..7ec0f8ed35 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_aria.html @@ -0,0 +1,200 @@ +<html> + +<head> + <title>nsIAccessible actions testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + + <script type="application/javascript"> + function doTest() { + var actionsArray = [ + { + ID: "clickable", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "button", + actionName: "press", + events: CLICK_EVENTS, + }, + { + ID: "checkbox_unchecked", + actionName: "check", + events: CLICK_EVENTS, + }, + { + ID: "checkbox_checked", + actionName: "uncheck", + events: CLICK_EVENTS, + }, + { + ID: "checkbox_mixed", + actionName: "cycle", + events: CLICK_EVENTS, + }, + { + ID: "combobox_collapsed", + actionName: "open", + events: CLICK_EVENTS, + }, + { + ID: "combobox_expanded", + actionName: "close", + events: CLICK_EVENTS, + }, + { + ID: "link", + actionName: "jump", + events: CLICK_EVENTS, + }, + { + ID: "menuitem", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "menuitemcheckbox", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "menuitemradio", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "option", + actionName: "select", + events: CLICK_EVENTS, + }, + { + ID: "radio", + actionName: "select", + events: CLICK_EVENTS, + }, + { + ID: "switch_unchecked", + actionName: "check", + events: CLICK_EVENTS, + }, + { + ID: "switch_checked", + actionName: "uncheck", + events: CLICK_EVENTS, + }, + { + ID: "tab", + actionName: "switch", + events: CLICK_EVENTS, + }, + { + ID: "textbox", + actionName: "activate", + events: CLICK_EVENTS, + }, + { + ID: "treeitem", + actionName: "activate", + events: CLICK_EVENTS, + }, + { + ID: "sortable", + actionName: "sort", + events: CLICK_EVENTS, + }, + { + ID: "expandable", + actionName: "expand", + events: CLICK_EVENTS, + }, + { + ID: "collapseable", + actionName: "collapse", + events: CLICK_EVENTS, + }, + ]; + testActions(actionsArray); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=410765" + title="nsIAccessible actions testing"> + Mozilla Bug 410765 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="clickable" onclick="">Clickable text</div> + + <div id="button" role="button">Button</div> + + <div id="checkbox_unchecked" role="checkbox">Checkbox</div> + + <div id="checkbox_checked" role="checkbox" aria-checked="true">Checkbox</div> + + <div id="checkbox_mixed" role="checkbox" aria-checked="mixed">Checkbox</div> + + <div id="combobox_collapsed" role="combobox"> + <div id="option" role="option">Option of collapsed combobox</div> + </div> + + <div id="combobox_expanded" role="combobox" aria-expanded="true"> + <div role="option">Option of expanded combobox</div> + </div> + + <div id="link" role="link">Link</div> + + <div role="menu"> + <div id="menuitem" role="menuitem">Menuitem</div> + <div id="menuitemcheckbox" role="menuitemcheckbox">Menuitem checkbox</div> + <div id="menuitemradio" role="menuitemradio">Menuitem radio</div> + </div> + + <div role="radiogroup"> + <div id="radio" role="radio">Radio</div> + </div> + + <div id="switch_unchecked" role="switch">Switch</div> + + <div id="switch_checked" role="switch" aria-checked="true">Switch</div> + + <div role="tablist"> + <div id="tab" role="tab">Tab</div> + </div> + + <div id="textbox" role="textbox">Textbox</div> + + <div role="tree"> + <div id="treeitem" role="treeitem">Treeitem</div> + </div> + + <div role="grid"> + <div id="sortable" role="columnheader" aria-sort="ascending"> + Columnheader + </div> + </div> + + <div id="expandable" aria-expanded="false">collapsed</div> + <div id="collapseable" aria-expanded="true">expanded</div> +</body> +</html> diff --git a/accessible/tests/mochitest/actions/test_controls.html b/accessible/tests/mochitest/actions/test_controls.html new file mode 100644 index 0000000000..8b6f413619 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_controls.html @@ -0,0 +1,107 @@ +<html> + +<head> + <title>nsIAccessible actions testing for inputs</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + + <script type="application/javascript"> + function doTest() { + var actionsArray = [ + { + ID: "button", + actionName: "press", + events: CLICK_EVENTS, + }, + { + ID: "input_button", + actionName: "press", + events: CLICK_EVENTS, + }, + { + ID: "checkbox_unchecked", + actionName: "check", + events: CLICK_EVENTS, + }, + { + ID: "checkbox_checked", + actionName: "uncheck", + events: CLICK_EVENTS, + }, + { + ID: "checkbox_mixed", + actionName: "cycle", + events: CLICK_EVENTS, + }, + { + ID: "radio", + actionName: "select", + events: CLICK_EVENTS, + }, + { + ID: "textarea", + actionName: "activate", + events: FOCUS_EVENT, + }, + { + ID: "textinput", + actionName: "activate", + events: FOCUS_EVENT, + }, + + ]; + document.getElementById("checkbox_mixed").indeterminate = true; + + testActions(actionsArray); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=477975" + title="nsIAccessible actions testing"> + Mozilla Bug 477975 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <button id="button">Button</button> + + <input id="input_button" type="button" value="normal"> + + <input id="checkbox_unchecked" type="checkbox">Checkbox</input> + + <input id="checkbox_checked" type="checkbox" checked="true">Checkbox</input> + + <input id="checkbox_mixed" type="checkbox">Checkbox</input> + + <fieldset> + <input id="radio" type="radio">Radio</input> + </fieldset> + + <textarea id="textarea" placeholder="What's happening?"></textarea> + + <input id="textinput" type="text"> +</body> +</html> diff --git a/accessible/tests/mochitest/actions/test_general.html b/accessible/tests/mochitest/actions/test_general.html new file mode 100644 index 0000000000..025b18f175 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_general.html @@ -0,0 +1,105 @@ +<html> + +<head> + <title>nsIAccessible actions testing on HTML elements</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + + <script type="application/javascript"> + function doTest() { + var actionsArray = [ + { + ID: "li_clickable1", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "li_clickable2", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "li_clickable3", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "onclick_img", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "label1", + actionName: "click", + events: CLICK_EVENTS, + }, + + ]; + + testActions(actionsArray); + + is(getAccessible("label1").firstChild.actionCount, 1, "label text should have 1 action"); + + getAccessible("onclick_img").takeFocus(); + is(getAccessible("link1").actionCount, 1, "links should have one action"); + is(getAccessible("link2").actionCount, 1, "link with onclick handler should have 1 action"); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=523789" + title="nsHTMLLiAccessible shouldn't be inherited from linkable accessible"> + Mozilla Bug 523789 + </a><br> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=423409" + title="Expose click action if mouseup and mousedown are registered"> + Mozilla Bug 423409 + </a> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=659620" + title="hang when trying to edit a page on wikimo with NVDA running"> + Mozilla Bug 659620 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <ul> + <li id="li_clickable1" onclick="">Clickable list item</li> + <li id="li_clickable2" onmousedown="">Clickable list item</li> + <li id="li_clickable3" onmouseup="">Clickable list item</li> + </ul> + + <!-- linkable accessibles --> + <img id="onclick_img" onclick="" src="../moz.png"> + + <a id="link1" href="www">linkable textleaf accessible</a> + <div id="link2" onclick="">linkable textleaf accessible</div> + + <div> + <label for="TextBox_t2" id="label1"> + <span>Explicit</span> + </label> + <input name="in2" id="TextBox_t2" type="text" maxlength="17"> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/actions/test_general.xhtml b/accessible/tests/mochitest/actions/test_general.xhtml new file mode 100644 index 0000000000..956688afa9 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_general.xhtml @@ -0,0 +1,172 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<?xml-stylesheet href="../nsIAccessible_name.css" + type="text/css"?> + + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="nsIAccessible actions testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../actions.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + + <script type="application/javascript"> + <![CDATA[ + //gA11yEventDumpToConsole = true; + //enableLogging("tree,verbose"); // debug + + if (navigator.platform.startsWith("Mac")) { + SimpleTest.expectAssertions(0, 1); + } else { + SimpleTest.expectAssertions(0, 1); + } + + function doTest() + { + var actionsArray = [ + { + ID: "menu", + actionName: "click", + events: CLICK_EVENTS, + // Wait for focus event, it guarantees us the submenu tree is created, + // that's necessary for next test. + eventSeq: [ + new invokerChecker(EVENT_FOCUS, getNode("menu")) + ] + }, + { + ID: "submenu", + actionName: "click", + events: CLICK_EVENTS + }, + { + ID: "menuitem", + actionName: "click", + events: XUL_EVENTS + }, + { + ID: "button", + actionName: "press", + events: XUL_EVENTS + }, + { + ID: "buttonmenu", + actionName: "press", + events: CLICK_EVENTS + }, + { + ID: "name_entry_label", + actionName: "click", + events: CLICK_EVENTS + }, + { + ID: "labelWithPopup", + actionName: "click", + events: CLICK_EVENTS + }, + { + ID: "toolbarbutton_label", + actionName: "click", + targetID: "toolbarbutton", + events: XUL_EVENTS, + allowBubbling: true + }, + { + ID: "menulist_label", + actionName: "click", + // focusChecker expects a unique focus event. However, there might + // still be pending focus events not caught by previous tests. + eventSeq: [ + new invokerChecker(EVENT_FOCUS, getNode("menulist")) + ] + }/*, // XXX: bug 490288 + { + ID: "buttonmenu_item", + actionName: "click", + events: CLICK_EVENTS + }*/ + ]; + + is(getAccessible("name_entry_label").firstChild.actionCount, 1, "label text should have 1 action"); + + testActions(actionsArray); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=410765" + title="nsIAccessible actions testing"> + Mozilla Bug 410765 + </a> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=504252" + title="Expose STATE_HASPOPUP on XUL elements that have an @popup attribute"> + Mozilla Bug 504252 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menubar> + <menu label="menu" id="menu"> + <menupopup> + <menuitem label="menu item" id="menuitem"/> + <menu label="submenu" id="submenu"> + <menupopup> + <menuitem label="menu item"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menubar> + + <button label="button" id="button"/> + + <button type="menu" id="buttonmenu" label="button"> + <menupopup> + <menuitem label="item1" id="buttonmenu_item"/> + <menuitem label="item1"/> + </menupopup> + </button> + + <label id="labelWithPopup" value="file name" + popup="fileContext" + tabindex="0"/> + <hbox> + <label id="name_entry_label" value="Name" control="name_entry"/> + <html:input id="name_entry"/> + </hbox> + <toolbarbutton id="toolbarbutton"> + <label id="toolbarbutton_label">toolbarbutton</label> + </toolbarbutton> + <hbox> + <label id="menulist_label" control="menulist">menulist</label> + <menulist id="menulist"/> + </hbox> + </vbox> + </hbox> +</window> + diff --git a/accessible/tests/mochitest/actions/test_keys.html b/accessible/tests/mochitest/actions/test_keys.html new file mode 100644 index 0000000000..acacb34c09 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_keys.html @@ -0,0 +1,57 @@ +<html> + +<head> + <meta http-equiv="Content-Type" content="text/html;charset=utf-8" /> + <title>Keyboard shortcuts tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + + <script type="application/javascript"> + function testAcessKey(aAccOrElmOrID, aKey) { + var acc = getAccessible(aAccOrElmOrID); + if (!acc) + return; + + is(acc.accessKey, aKey, + "Wrong keyboard shortcut on " + prettyName(aAccOrElmOrID)); + } + + function doTest() { + testAcessKey("input1", ""); + testAcessKey("input2", MAC ? "⌃⌥b" : "Alt+Shift+b"); + testAcessKey("link", MAC ? "⌃⌥l" : "Alt+Shift+l"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=381599" + title="Inverse relations cache"> + Mozilla Bug 381599 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <label accesskey="a"> + <input id="input1"/> + </label> + <label accesskey="b" for="input2"> + <input id="input2"/> + <a id="link" accesskey="l">link</a> +</body> +</html> diff --git a/accessible/tests/mochitest/actions/test_keys.xhtml b/accessible/tests/mochitest/actions/test_keys.xhtml new file mode 100644 index 0000000000..edba3eea76 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_keys.xhtml @@ -0,0 +1,107 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Accessible XUL access keys and shortcut keys tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + function openMenu(aMenuID, aMenuitemID) + { + this.menuNode = getNode(aMenuID); + this.menuitemNode = getNode(aMenuitemID); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, this.menuNode) + ]; + + this.invoke = function openMenu_invoke() + { + // Show menu. + this.menuNode.open = true; + } + + this.finalCheck = function openMenu_finalCheck() + { + var menu = getAccessible(aMenuID); + is(menu.accessKey, (MAC ? "u" : "Alt+u"), + "Wrong accesskey on " + prettyName(this.menuitemNode)); + + var menuitem = getAccessible(aMenuitemID); + is(menuitem.accessKey, "p", + "Wrong accesskey on " + prettyName(this.menuitemNode)); + is(menuitem.keyboardShortcut, (MAC ? "⌃l" : "Ctrl+l"), + "Wrong keyboard shortcut on " + prettyName(this.menuitemNode)); + } + + this.getID = function openMenu_getID() + { + return "menuitem accesskey and shortcut test " + + prettyName(this.menuItemNode); + } + } + + var gQueue = null; + function doTest() + { + // HTML element should get accessKey from associated XUL label. + let input = getAccessible("input"); + is(input.accessKey, (MAC ? "⌃⌥i" : "Alt+Shift+i"), + "Wrong accessKey on input"); + + gQueue = new eventQueue(); + gQueue.push(new openMenu("menu", "menuitem")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=672092" + title="Reorganize access key and keyboard shortcut handling code"> + Mozilla Bug 672092 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <label control="input" accesskey="i">input</label> + <html:input id="input"/> + + <keyset> + <key key="l" modifiers="control" id="key1"/> + </keyset> + + <menubar> + <menu label="menu" id="menu" accesskey="u"> + <menupopup> + <menuitem accesskey="p" key="key1" label="item1" id="menuitem"/> + </menupopup> + </menu> + </menubar> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/actions/test_link.html b/accessible/tests/mochitest/actions/test_link.html new file mode 100644 index 0000000000..4dc0bab678 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_link.html @@ -0,0 +1,141 @@ +<html> + +<head> + <title>nsIAccessible actions testing on HTML links (HTML:a)</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + + <script type="application/javascript"> + function getAnchorTargetDocumentAcc() { + var thisTabDocAcc = getTabDocAccessible(); + var thisDocTabPanelAcc = thisTabDocAcc.parent.parent; + var tabPanelsAcc = thisDocTabPanelAcc.parent; + var newDocTabPanelAcc = tabPanelsAcc.firstChild; + var nextAcc = newDocTabPanelAcc; + + while ((nextAcc = nextAcc.nextSibling)) { + // Find the last accessible for a browser with about:mozilla loaded. + if (nextAcc.firstChild.DOMNode.currentURI.spec == "about:mozilla") { + newDocTabPanelAcc = nextAcc; + } + } + + return newDocTabPanelAcc.firstChild.firstChild; + } + + function linkChecker(aID) { + this.type = EVENT_DOCUMENT_LOAD_COMPLETE; + this.__defineGetter__("target", getAnchorTargetDocumentAcc); + + this.check = function linkChecker_check() { + var anchorTargetWindow = + getAccessible(getAnchorTargetDocumentAcc(), [nsIAccessibleDocument]). + window; + anchorTargetWindow.close(); + }; + + this.getID = function linkChecker_getID() { + return "link '" + aID + "' states check "; + }; + } + + // gA11yEventDumpToConsole = true; + // enableLogging("tree,eventTree,verbose"); + + function doTest() { + var actionsArray = [ + { + ID: "link1", + actionName: "jump", + events: CLICK_EVENTS, + eventSeq: [ + new linkChecker("link1"), + ], + }, + { + ID: "img1", + targetID: "link1", + actionName: "jump", + events: CLICK_EVENTS, + eventSeq: [ + new linkChecker("link1"), + ], + }, + { + ID: "link2", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "img2", + targetID: "link2", + actionName: "jump", + events: CLICK_EVENTS, + }, + { + ID: "link3", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "img3", + targetID: "link3", + actionName: "jump", + events: CLICK_EVENTS, + }, + { + ID: "link4", + actionName: "click", + events: CLICK_EVENTS, + }, + { + ID: "img4", + targetID: "link4", + actionName: "jump", + events: CLICK_EVENTS, + }, + ]; + testActions(actionsArray); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=423409" + title="Expose click action if mouseup and mousedown are registered"> + Mozilla Bug 423409 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <a href="about:mozilla" id="link1" target="_blank" rel="opener"> + <img src="../moz.png" id="img1"> + </a> + <a id="link2" onmousedown=""> + <img src="../moz.png" id="img2"> + </a> + <a id="link3" onclick=""> + <img src="../moz.png" id="img3"> + </a> + <a id="link4" onmouseup=""> + <img src="../moz.png" id="img4"> + </a> +</body> +</html> diff --git a/accessible/tests/mochitest/actions/test_media.html b/accessible/tests/mochitest/actions/test_media.html new file mode 100644 index 0000000000..f6232e51e9 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_media.html @@ -0,0 +1,130 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=483573 +--> +<head> + <title>HTML5 audio/video tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + + // gA11yEventDumpID = "eventDump"; + // gA11yEventDumpToConsole = true; // debug stuff + + function focusChecker(aAcc) { + this.type = EVENT_FOCUS; + this.target = aAcc; + this.getID = function focusChecker_getID() { + return "focus handling"; + }; + this.check = function focusChecker_check(aEvent) { + testStates(this.target, STATE_FOCUSED); + }; + } + + function nameChecker(aAcc, aName) { + this.type = EVENT_NAME_CHANGE; + this.target = aAcc; + this.getID = function nameChecker_getID() { + return "name change handling"; + }; + this.check = function nameChecker_check(aEvent) { + is(aEvent.accessible.name, aName, + "Wrong name of " + prettyName(aEvent.accessible) + " on focus"); + }; + } + + async function loadAudioSource() { + /** + * Setting the source dynamically and wait for it to load, + * so we can test the accessibility tree of the control in its ready and + * stable state. + * + * See bug 1484048 comment 25 for discussion on how it switches UI when + * loading a statically declared source. + */ + await new Promise(resolve => { + let el = document.getElementById("audio"); + el.addEventListener("canplaythrough", resolve, {once: true}); + el.src = "../bug461281.ogg"; + }); + + doTest(); + } + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // test actions of audio controls + + todo(false, "Focus test are disabled until bug 494175 is fixed."); + + var audioElm = getAccessible("audio"); + var playBtn = audioElm.firstChild; + // var scrubber = playBtn.nextSibling.nextSibling.nextSibling; + var muteBtn = audioElm.lastChild.previousSibling; + + var actions = [ + { + ID: muteBtn, + actionName: "press", + eventTarget: "element", + eventSeq: [ + // new focusChecker(muteBtn), + new nameChecker(muteBtn, "Unmute"), + ], + }, + // { + // ID: scrubber, + // actionName: "activate", + // events: null, + // eventSeq: [ + // new focusChecker(scrubber) + // ] + // }, + { + ID: playBtn, + actionName: "press", + eventTarget: "element", + eventSeq: [ + // new focusChecker(playBtn), + new nameChecker(playBtn, "Pause"), + ], + }, + ]; + + testActions(actions); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(loadAudioSource); + </script> +</head> +<body> + + <a target="_blank" rel="opener" + title="Expose HTML5 video and audio elements' embedded controls through accessibility APIs" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=483573">Mozilla Bug 483573</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <audio id="audio" controls="true"></audio> + + <div id="eventDump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/actions/test_select.html b/accessible/tests/mochitest/actions/test_select.html new file mode 100644 index 0000000000..3cc7c71849 --- /dev/null +++ b/accessible/tests/mochitest/actions/test_select.html @@ -0,0 +1,103 @@ +<html> + +<head> + <title>nsIAccessible actions testing for HTML select</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; // debugging + function doTest() { + var actionsArray = [ + { + ID: "lb_apple", + actionIndex: 0, + actionName: "select", + events: CLICK_EVENTS, + eventSeq: [ + new focusChecker("lb_apple"), + ], + }, + { + ID: "combobox", + actionIndex: 0, + actionName: "open", + events: CLICK_EVENTS, + eventSeq: [ + new focusChecker("cb_orange"), + ], + }, + { + ID: "cb_apple", + actionIndex: 0, + actionName: "select", + events: CLICK_EVENTS, + eventSeq: [ + new focusChecker("combobox"), + ], + }, + { + ID: "combobox", + actionIndex: 0, + actionName: "open", + events: CLICK_EVENTS, + eventSeq: [ + new focusChecker("cb_apple"), + ], + }, + { + ID: "combobox", + actionIndex: 0, + actionName: "close", + events: CLICK_EVENTS, + eventSeq: [ + new focusChecker("combobox"), + ], + }, + ]; + + testActions(actionsArray); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="listbox" size="2"> + <option id="lb_orange">orange</option> + <option id="lb_apple">apple</option> + </select> + + <select id="combobox"> + <option id="cb_orange">orange</option> + <option id="cb_apple">apple</option> + </select> +</body> +</html> diff --git a/accessible/tests/mochitest/actions/test_tree.xhtml b/accessible/tests/mochitest/actions/test_tree.xhtml new file mode 100644 index 0000000000..17710cbdce --- /dev/null +++ b/accessible/tests/mochitest/actions/test_tree.xhtml @@ -0,0 +1,127 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tree actions tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../actions.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Accessible tree testers + + function stateFocusChecker(aAcc, aStates) + { + this.__proto__ = new focusChecker(aAcc); + + this.check = function focusChecker_check(aEvent) + { + var states = aStates ? aStates : 0; + testStates(this.target, STATE_FOCUSED | STATE_SELECTED | states); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; + //gA11yEventDumpToConsole = true; // debug + + function doTest() + { + var treeNode = getNode("tree"); + + var treeBodyNode = treeNode.treeBody; + + var tree = getAccessible(treeNode); + var expandedTreeItem = tree.getChildAt(2); + var collapsedTreeItem = tree.getChildAt(5); + + var actions = [ + { + ID: expandedTreeItem, + actionName: "activate", + actionIndex: 0, + events: CLICK_EVENTS, + targetID: treeBodyNode, + eventSeq: [ + new stateFocusChecker(expandedTreeItem, STATE_EXPANDED) + ] + }, + { + ID: collapsedTreeItem, + actionName: "expand", + actionIndex: 1, + events: CLICK_EVENTS, + targetID: treeBodyNode, + checkOnClickEvent: function check(aEvent) + { + testStates(this.ID, STATE_EXPANDED); + } + }, + { + ID: collapsedTreeItem, + actionName: "collapse", + actionIndex: 1, + events: CLICK_EVENTS, + targetID: treeBodyNode, + checkOnClickEvent: function check(aEvent) + { + testStates(this.ID, STATE_COLLAPSED); + } + } + ]; + + testActions(actions); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "tree", new nsTreeTreeView()); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Mozilla Bug 503727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tree id="tree" flex="1" minheight="100px"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/actions/test_treegrid.xhtml b/accessible/tests/mochitest/actions/test_treegrid.xhtml new file mode 100644 index 0000000000..1c6e1bb8aa --- /dev/null +++ b/accessible/tests/mochitest/actions/test_treegrid.xhtml @@ -0,0 +1,190 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<?xml-stylesheet href="../treeview.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tree actions tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../actions.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Accessible tree testers + + function focusChecker(aAcc, aStates) + { + this.type = EVENT_FOCUS; + this.target = aAcc; + this.getID = function focusChecker_getID() + { + return "focus handling"; + } + this.check = function focusChecker_check(aEvent) + { + var states = aStates ? aStates : 0; + testStates(this.target, STATE_FOCUSED | STATE_SELECTED | states); + } + } + + function stateChangeChecker(aAcc, aIsEnabled) + { + this.type = EVENT_STATE_CHANGE; + this.target = aAcc; + this.getID = function stateChangeChecker_getID() + { + return "state change handling"; + } + this.check = function stateChangeChecker_check(aEvent) + { + if (aIsEnabled) + testStates(this.target, STATE_CHECKED); + else + testStates(this.target, STATE_CHECKABLE, 0, STATE_CHECKED); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTestActions() + { + var treeNode = getNode("tabletree"); + + var treeBodyNode = treeNode.treeBody; + treeNode.focus(); + + var tree = getAccessible(treeNode); + + var expandedTreeItem = tree.getChildAt(2); + var collapsedTreeItem = tree.getChildAt(5); + var cycleCell = expandedTreeItem.getChildAt(0); + var checkableCell = expandedTreeItem.getChildAt(3); + + var actions = [ + { + ID: expandedTreeItem, + actionName: "activate", + actionIndex: 0, + events: CLICK_EVENTS, + targetID: treeBodyNode, + eventSeq: [ + new focusChecker(expandedTreeItem, STATE_EXPANDED) + ] + }, + { + ID: collapsedTreeItem, + actionName: "expand", + actionIndex: 1, + events: CLICK_EVENTS, + targetID: treeBodyNode, + check: function check(aEvent) + { + testStates(this.ID, STATE_EXPANDED); + } + }, + { + ID: collapsedTreeItem, + actionName: "collapse", + actionIndex: 1, + events: CLICK_EVENTS, + targetID: treeBodyNode, + check: function check(aEvent) + { + testStates(this.ID, STATE_COLLAPSED); + } + }, + { + ID: cycleCell, + actionName: "cycle", + actionIndex: 0, + events: CLICK_EVENTS, + targetID: treeBodyNode + }, + { + ID: checkableCell, + actionName: "uncheck", + actionIndex: 0, + events: CLICK_EVENTS, + targetID: treeBodyNode, + eventSeq: [ + new stateChangeChecker(checkableCell, false) + ] + }, + { + ID: checkableCell, + actionName: "check", + actionIndex: 0, + events: CLICK_EVENTS, + targetID: treeBodyNode, + eventSeq: [ + new stateChangeChecker(checkableCell, true) + ] + } + ]; + + testActions(actions); // Will call SimpleTest.finish(); + } + + // gA11yEventDumpID = "debug"; + + function doTest() + { + var treeNode = getNode("tabletree"); + waitForEvent(EVENT_REORDER, treeNode, doTestActions); + treeNode.view = new nsTreeTreeView(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" rel="opener" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Mozilla Bug 503727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tree id="tabletree" flex="1" editable="true"> + <treecols> + <treecol id="tabletree_col1" cycler="true" label="cycler"/> + <treecol id="tabletree_col2" flex="1" primary="true" label="column1"/> + <treecol id="tabletree_col3" flex="1" label="column2"/> + <treecol id="tabletree_col4" flex="1" label="checker" + type="checkbox" editable="true"/> + </treecols> + <treechildren/> + </tree> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/aom/a11y.ini b/accessible/tests/mochitest/aom/a11y.ini new file mode 100644 index 0000000000..03085c1deb --- /dev/null +++ b/accessible/tests/mochitest/aom/a11y.ini @@ -0,0 +1,3 @@ +[DEFAULT] + +[test_general.html] diff --git a/accessible/tests/mochitest/aom/test_general.html b/accessible/tests/mochitest/aom/test_general.html new file mode 100644 index 0000000000..dc63fb659b --- /dev/null +++ b/accessible/tests/mochitest/aom/test_general.html @@ -0,0 +1,208 @@ +<!DOCTYPE HTML> +<html> +<head> + <meta charset="utf-8"> + <title>Accessibility API: generic</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css"> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script> + "use strict"; + + SimpleTest.waitForExplicitFinish(); + const finish = SimpleTest.finish.bind(SimpleTest); + enablePref() + .then(createIframe) + .then(checkImplementation) + .catch(err => { + dump(`${err}: ${err.stack}`); + finish(); + }); + + function enablePref() { + const ops = { + "set": [ + [ "accessibility.AOM.enabled", true ], + ], + }; + return SpecialPowers.pushPrefEnv(ops); + } + + // WebIDL conditional annotations for an interface are evaluated once per + // global, so we need to create an iframe to see the effects of calling + // enablePref(). + function createIframe() { + return new Promise((resolve) => { + let iframe = document.createElement("iframe"); + iframe.src = `data:text/html,<html><body>hey</body></html>`; + iframe.onload = () => resolve(iframe.contentDocument); + document.body.appendChild(iframe); + document.body.offsetTop; // We rely on the a11y tree being created + // already, and it's created off layout. + }); + } + + function testStringProp(anode, prop) { + is(anode[prop], null, `anode.${prop} should be null`); + let text = "This is a string test"; + anode[prop] = text; + is(anode[prop], text, `anode.${prop} was assigned "${text}"`); + anode[prop] = null; + is(anode[prop], null, `anode.${prop} was assigned null`); + } + + function testBoolProp(anode, prop) { + is(anode[prop], null, `anode.${prop} should be null`); + anode[prop] = true; + is(anode[prop], true, `anode.${prop} was assigned true`); + anode[prop] = false; + is(anode[prop], false, `anode.${prop} was assigned false`); + anode[prop] = null; + is(anode[prop], null, `anode.${prop} was assigned null`); + } + + function testDoubleProp(anode, prop) { + is(anode[prop], null, `anode.${prop} should be null`); + anode[prop] = Number.MAX_VALUE; + is(anode[prop], Number.MAX_VALUE, `anode.${prop} was assigned ${Number.MAX_VALUE}`); + anode[prop] = null; + is(anode[prop], null, `anode.${prop} was assigned null`); + } + + function testIntProp(anode, prop) { + is(anode[prop], null, `anode.${prop} should be null`); + anode[prop] = -1; + is(anode[prop], -1, `anode.${prop} was assigned -1`); + anode[prop] = null; + is(anode[prop], null, `anode.${prop} was assigned null`); + } + + function testUIntProp(anode, prop) { + is(anode[prop], null, `anode.${prop} should be null`); + anode[prop] = 4294967295; + is(anode[prop], 4294967295, `anode.${prop} was assigned 4294967295`); + anode[prop] = null; + is(anode[prop], null, `anode.${prop} was assigned null`); + } + + function testRelationProp(anode, node, prop) { + is(anode[prop], null, `anode.${prop} should be null`); + anode[prop] = node.accessibleNode; + is(anode[prop], node.accessibleNode, `anode.${prop} was assigned AccessibleNode`); + anode[prop] = null; + is(anode[prop], null, `anode.${prop} was assigned null`); + } + // Check that the WebIDL is as expected. + function checkImplementation(ifrDoc) { + let anode = ifrDoc.accessibleNode; + ok(anode, "DOM document has accessible node"); + + is(anode.computedRole, "document", "correct role of a document accessible node"); + is(anode.DOMNode, ifrDoc, "correct DOM Node of a document accessible node"); + + // States may differ depending on the document state, for example, if it is + // loaded or is loading still. + var states = null; + switch (anode.states.length) { + case 5: + states = [ + "readonly", "focusable", "opaque", "enabled", "sensitive", + ]; + break; + case 6: + states = [ + "readonly", "busy", "focusable", "opaque", "enabled", "sensitive", + ]; + break; + case 7: + states = [ + "readonly", "busy", "focusable", "opaque", "stale", "enabled", "sensitive", + ]; + break; + default: + ok(false, "Unexpected amount of states: " + JSON.stringify(anode.states)); + } + if (states) { + for (let i = 0; i < states.length; i++) { + is(anode.states[i], states[i], `${states[i]} state is expected at ${i}th index`); + } + } + + ok(anode.is("document", "focusable"), + "correct role and state on an accessible node"); + + is(anode.get("explicit-name"), "true", + "correct object attribute value on an accessible node"); + + ok(anode.has("explicit-name"), + "object attributes are present"); + + var attrs = [ "explicit-name" ]; + if (anode.attributes.length > 1) { + attrs = [ + "margin-left", "text-align", "text-indent", "margin-right", + "tag", "margin-top", "margin-bottom", "display", + "explicit-name", + ]; + } + + is(anode.attributes.length, attrs.length, "correct number of attributes"); + for (let i = 0; i < attrs.length; i++) { + ok(attrs.includes(anode.attributes[i]), + `${anode.attributes[i]} attribute is expected and found`); + } + + const strProps = ["autocomplete", "checked", "current", "hasPopUp", "invalid", + "keyShortcuts", "label", "live", "orientation", "placeholder", + "pressed", "relevant", "role", "roleDescription", "sort", + "valueText"]; + + for (const strProp of strProps) { + testStringProp(anode, strProp); + } + + const boolProps = ["atomic", "busy", "disabled", "expanded", "hidden", "modal", + "multiline", "multiselectable", "readOnly", "required", "selected"]; + + for (const boolProp of boolProps) { + testBoolProp(anode, boolProp); + } + + const doubleProps = ["valueMax", "valueMin", "valueNow"]; + + for (const doubleProp of doubleProps) { + testDoubleProp(anode, doubleProp); + } + + const intProps = ["colCount", "rowCount", "setSize"]; + + for (const intProp of intProps) { + testIntProp(anode, intProp); + } + + const uintProps = ["colIndex", "colSpan", "level", "posInSet", "rowIndex", "rowSpan"]; + + for (const uintProp of uintProps) { + testUIntProp(anode, uintProp); + } + + // Check if an AccessibleNode is properly cached. + let node = ifrDoc.createElement("div"); + anode = node.accessibleNode; + is(anode, node.accessibleNode, "an AccessibleNode is properly cached"); + + // Adopting node to another document doesn't change .accessibleNode + let anotherDoc = ifrDoc.implementation.createDocument("", "", null); + let adopted_node = anotherDoc.adoptNode(node); + is(anode, adopted_node.accessibleNode, "adopting node to another document doesn't change node.accessibleNode"); + + const relationProps = ["activeDescendant", "details", "errorMessage"]; + + for (const relationProp of relationProps) { + testRelationProp(anode, node, relationProp); + } + + finish(); + } + </script> +</head> diff --git a/accessible/tests/mochitest/attributes.js b/accessible/tests/mochitest/attributes.js new file mode 100644 index 0000000000..d41a56f959 --- /dev/null +++ b/accessible/tests/mochitest/attributes.js @@ -0,0 +1,490 @@ +/* import-globals-from common.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Object attributes. + +/** + * Test object attributes. + * + * @param aAccOrElmOrID [in] the accessible identifier + * @param aAttrs [in] the map of expected object attributes + * (name/value pairs) + * @param aSkipUnexpectedAttrs [in] points this function doesn't fail if + * unexpected attribute is encountered + */ +function testAttrs(aAccOrElmOrID, aAttrs, aSkipUnexpectedAttrs) { + testAttrsInternal(aAccOrElmOrID, aAttrs, aSkipUnexpectedAttrs); +} + +/** + * Test object attributes that must not be present. + * + * @param aAccOrElmOrID [in] the accessible identifier + * @param aAbsentAttrs [in] map of attributes that should not be + * present (name/value pairs) + */ +function testAbsentAttrs(aAccOrElmOrID, aAbsentAttrs) { + testAttrsInternal(aAccOrElmOrID, {}, true, aAbsentAttrs); +} + +/** + * Test object attributes that aren't right, but should be (todo) + * + * @param aAccOrElmOrID [in] the accessible identifier + * @param aKey [in] attribute name + * @param aExpectedValue [in] expected attribute value + */ +function todoAttr(aAccOrElmOrID, aKey, aExpectedValue) { + var accessible = getAccessible(aAccOrElmOrID); + if (!accessible) { + return; + } + + var attrs = null; + try { + attrs = accessible.attributes; + } catch (e) {} + + todo_is(attrs.getStringProperty(aKey), aExpectedValue, "attributes match"); +} + +/** + * Test CSS based object attributes. + */ +function testCSSAttrs(aID) { + var node = document.getElementById(aID); + var computedStyle = document.defaultView.getComputedStyle(node); + + var attrs = { + display: computedStyle.display, + "text-align": computedStyle.textAlign, + "text-indent": computedStyle.textIndent, + "margin-left": computedStyle.marginLeft, + "margin-right": computedStyle.marginRight, + "margin-top": computedStyle.marginTop, + "margin-bottom": computedStyle.marginBottom, + }; + testAttrs(aID, attrs, true); +} + +/** + * Test the accessible that it doesn't have CSS-based object attributes. + */ +function testAbsentCSSAttrs(aID) { + var attrs = { + display: "", + "text-align": "", + "text-indent": "", + "margin-left": "", + "margin-right": "", + "margin-top": "", + "margin-bottom": "", + }; + testAbsentAttrs(aID, attrs); +} + +/** + * Test group object attributes (posinset, setsize and level) and + * nsIAccessible::groupPosition() method. + * + * @param aAccOrElmOrID [in] the ID, DOM node or accessible + * @param aPosInSet [in] the value of 'posinset' attribute + * @param aSetSize [in] the value of 'setsize' attribute + * @param aLevel [in, optional] the value of 'level' attribute + */ +function testGroupAttrs(aAccOrElmOrID, aPosInSet, aSetSize, aLevel) { + var acc = getAccessible(aAccOrElmOrID); + var levelObj = {}, + posInSetObj = {}, + setSizeObj = {}; + acc.groupPosition(levelObj, setSizeObj, posInSetObj); + + if (aPosInSet && aSetSize) { + is( + posInSetObj.value, + aPosInSet, + "Wrong group position (posinset) for " + prettyName(aAccOrElmOrID) + ); + is( + setSizeObj.value, + aSetSize, + "Wrong size of the group (setsize) for " + prettyName(aAccOrElmOrID) + ); + + let attrs = { + posinset: String(aPosInSet), + setsize: String(aSetSize), + }; + testAttrs(aAccOrElmOrID, attrs, true); + } + + if (aLevel) { + is( + levelObj.value, + aLevel, + "Wrong group level for " + prettyName(aAccOrElmOrID) + ); + + let attrs = { level: String(aLevel) }; + testAttrs(aAccOrElmOrID, attrs, true); + } +} + +function testGroupParentAttrs(aAccOrElmOrID, aChildItemCount, aIsHierarchical) { + testAttrs( + aAccOrElmOrID, + { "child-item-count": String(aChildItemCount) }, + true + ); + + if (aIsHierarchical) { + testAttrs(aAccOrElmOrID, { hierarchical: "true" }, true); + } else { + testAbsentAttrs(aAccOrElmOrID, { hierarchical: "true" }); + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// Text attributes. + +/** + * Test text attributes. + * + * @param aID [in] the ID of DOM element having text + * accessible + * @param aOffset [in] the offset inside text accessible to fetch + * text attributes + * @param aAttrs [in] the map of expected text attributes + * (name/value pairs) exposed at the offset + * @param aDefAttrs [in] the map of expected text attributes + * (name/value pairs) exposed on hyper text + * accessible + * @param aStartOffset [in] expected start offset where text attributes + * are applied + * @param aEndOffset [in] expected end offset where text attribute + * are applied + * @param aSkipUnexpectedAttrs [in] points the function doesn't fail if + * unexpected attribute is encountered + */ +function testTextAttrs( + aID, + aOffset, + aAttrs, + aDefAttrs, + aStartOffset, + aEndOffset, + aSkipUnexpectedAttrs +) { + var accessible = getAccessible(aID, [nsIAccessibleText]); + if (!accessible) { + return; + } + + var startOffset = { value: -1 }; + var endOffset = { value: -1 }; + + // do not include attributes exposed on hyper text accessible + var attrs = getTextAttributes( + aID, + accessible, + false, + aOffset, + startOffset, + endOffset + ); + + if (!attrs) { + return; + } + + var errorMsg = " for " + aID + " at offset " + aOffset; + + is(startOffset.value, aStartOffset, "Wrong start offset" + errorMsg); + is(endOffset.value, aEndOffset, "Wrong end offset" + errorMsg); + + compareAttrs(errorMsg, attrs, aAttrs, aSkipUnexpectedAttrs); + + // include attributes exposed on hyper text accessible + var expectedAttrs = {}; + for (let name in aAttrs) { + expectedAttrs[name] = aAttrs[name]; + } + + for (let name in aDefAttrs) { + if (!(name in expectedAttrs)) { + expectedAttrs[name] = aDefAttrs[name]; + } + } + + attrs = getTextAttributes( + aID, + accessible, + true, + aOffset, + startOffset, + endOffset + ); + + if (!attrs) { + return; + } + + compareAttrs(errorMsg, attrs, expectedAttrs, aSkipUnexpectedAttrs); +} + +/** + * Test default text attributes. + * + * @param aID [in] the ID of DOM element having text + * accessible + * @param aDefAttrs [in] the map of expected text attributes + * (name/value pairs) + * @param aSkipUnexpectedAttrs [in] points the function doesn't fail if + * unexpected attribute is encountered + */ +function testDefaultTextAttrs(aID, aDefAttrs, aSkipUnexpectedAttrs) { + var accessible = getAccessible(aID, [nsIAccessibleText]); + if (!accessible) { + return; + } + + var defAttrs = null; + try { + defAttrs = accessible.defaultTextAttributes; + } catch (e) {} + + if (!defAttrs) { + ok(false, "Can't get default text attributes for " + aID); + return; + } + + var errorMsg = ". Getting default text attributes for " + aID; + compareAttrs(errorMsg, defAttrs, aDefAttrs, aSkipUnexpectedAttrs); +} + +/** + * Test text attributes for wrong offset. + */ +function testTextAttrsWrongOffset(aID, aOffset) { + var res = false; + try { + var s = {}, + e = {}; + // Bug 1602031 + // eslint-disable-next-line no-undef + var acc = getAccessible(ID, [nsIAccessibleText]); + acc.getTextAttributes(false, 157, s, e); + } catch (ex) { + res = true; + } + + ok( + res, + "text attributes are calculated successfully at wrong offset " + + aOffset + + " for " + + prettyName(aID) + ); +} + +const kNormalFontWeight = function equalsToNormal(aWeight) { + return aWeight <= 400; +}; + +const kBoldFontWeight = function equalsToBold(aWeight) { + return aWeight > 400; +}; + +// The pt font size of the input element can vary by Linux distro. +const kInputFontSize = WIN + ? "10pt" + : MAC + ? "8pt" + : function() { + return true; + }; + +const kAbsentFontFamily = function(aFontFamily) { + return aFontFamily != "sans-serif"; +}; +const kInputFontFamily = function(aFontFamily) { + return aFontFamily != "sans-serif"; +}; + +const kMonospaceFontFamily = function(aFontFamily) { + return aFontFamily != "monospace"; +}; +const kSansSerifFontFamily = function(aFontFamily) { + return aFontFamily != "sans-serif"; +}; +const kSerifFontFamily = function(aFontFamily) { + return aFontFamily != "serif"; +}; + +const kCursiveFontFamily = LINUX ? "DejaVu Serif" : "Comic Sans MS"; + +/** + * Return used font from the given computed style. + */ +function fontFamily(aComputedStyle) { + var name = aComputedStyle.fontFamily; + switch (name) { + case "monospace": + return kMonospaceFontFamily; + case "sans-serif": + return kSansSerifFontFamily; + case "serif": + return kSerifFontFamily; + default: + return name; + } +} + +/** + * Build an object of default text attributes expected for the given accessible. + * + * @param aID [in] identifier of accessible + * @param aFontSize [in] font size + * @param aFontWeight [in, optional] kBoldFontWeight or kNormalFontWeight, + * default value is kNormalFontWeight + */ +function buildDefaultTextAttrs(aID, aFontSize, aFontWeight, aFontFamily) { + var elm = getNode(aID); + var computedStyle = document.defaultView.getComputedStyle(elm); + var bgColor = + computedStyle.backgroundColor == "rgba(0, 0, 0, 0)" + ? "rgb(255, 255, 255)" + : computedStyle.backgroundColor; + + var defAttrs = { + "font-style": computedStyle.fontStyle, + "font-size": aFontSize, + "background-color": bgColor, + "font-weight": aFontWeight ? aFontWeight : kNormalFontWeight, + color: computedStyle.color, + "font-family": aFontFamily ? aFontFamily : fontFamily(computedStyle), + "text-position": computedStyle.verticalAlign, + }; + + return defAttrs; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private. + +function getTextAttributes( + aID, + aAccessible, + aIncludeDefAttrs, + aOffset, + aStartOffset, + aEndOffset +) { + // This function expects the passed in accessible to already be queried for + // nsIAccessibleText. + var attrs = null; + try { + attrs = aAccessible.getTextAttributes( + aIncludeDefAttrs, + aOffset, + aStartOffset, + aEndOffset + ); + } catch (e) {} + + if (attrs) { + return attrs; + } + + ok(false, "Can't get text attributes for " + aID); + return null; +} + +function testAttrsInternal( + aAccOrElmOrID, + aAttrs, + aSkipUnexpectedAttrs, + aAbsentAttrs +) { + var accessible = getAccessible(aAccOrElmOrID); + if (!accessible) { + return; + } + + var attrs = null; + try { + attrs = accessible.attributes; + } catch (e) {} + + if (!attrs) { + ok(false, "Can't get object attributes for " + prettyName(aAccOrElmOrID)); + return; + } + + var errorMsg = " for " + prettyName(aAccOrElmOrID); + compareAttrs(errorMsg, attrs, aAttrs, aSkipUnexpectedAttrs, aAbsentAttrs); +} + +function compareAttrs( + aErrorMsg, + aAttrs, + aExpectedAttrs, + aSkipUnexpectedAttrs, + aAbsentAttrs +) { + // Check if all obtained attributes are expected and have expected value. + for (let prop of aAttrs.enumerate()) { + if (!(prop.key in aExpectedAttrs)) { + if (!aSkipUnexpectedAttrs) { + ok( + false, + "Unexpected attribute '" + + prop.key + + "' having '" + + prop.value + + "'" + + aErrorMsg + ); + } + } else { + var msg = "Attribute '" + prop.key + "' has wrong value" + aErrorMsg; + var expectedValue = aExpectedAttrs[prop.key]; + + if (typeof expectedValue == "function") { + ok(expectedValue(prop.value), msg); + } else { + is(prop.value, expectedValue, msg); + } + } + } + + // Check if all expected attributes are presented. + for (let name in aExpectedAttrs) { + var value = ""; + try { + value = aAttrs.getStringProperty(name); + } catch (e) {} + + if (!value) { + ok(false, "There is no expected attribute '" + name + "' " + aErrorMsg); + } + } + + // Check if all unexpected attributes are absent. + if (aAbsentAttrs) { + for (var name in aAbsentAttrs) { + var wasFound = false; + + for (let prop of aAttrs.enumerate()) { + if (prop.key == name) { + wasFound = true; + } + } + } + + ok( + !wasFound, + "There is an unexpected attribute '" + name + "' " + aErrorMsg + ); + } +} diff --git a/accessible/tests/mochitest/attributes/a11y.ini b/accessible/tests/mochitest/attributes/a11y.ini new file mode 100644 index 0000000000..0508c7289e --- /dev/null +++ b/accessible/tests/mochitest/attributes/a11y.ini @@ -0,0 +1,15 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_dpub_aria_xml-roles.html] +[test_graphics_aria_xml-roles.html] +[test_listbox.html] +[test_obj.html] +[test_obj_css.html] +[test_obj_css.xhtml] +[test_obj_group.html] +[test_obj_group.xhtml] +[test_obj_group_tree.xhtml] +[test_tag.html] +[test_xml-roles.html] diff --git a/accessible/tests/mochitest/attributes/test_dpub_aria_xml-roles.html b/accessible/tests/mochitest/attributes/test_dpub_aria_xml-roles.html new file mode 100644 index 0000000000..a6b4dd4840 --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_dpub_aria_xml-roles.html @@ -0,0 +1,120 @@ +<!DOCTYPE html> +<html> +<head> + <title>XML roles tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + function doTest() { + // DPub ARIA roles should be exposed via the xml-roles object attribute. + let dpub_attrs = [ + "doc-abstract", + "doc-acknowledgments", + "doc-afterword", + "doc-appendix", + "doc-backlink", + "doc-biblioentry", + "doc-bibliography", + "doc-biblioref", + "doc-chapter", + "doc-colophon", + "doc-conclusion", + "doc-cover", + "doc-credit", + "doc-credits", + "doc-dedication", + "doc-endnote", + "doc-endnotes", + "doc-epigraph", + "doc-epilogue", + "doc-errata", + "doc-example", + "doc-footnote", + "doc-foreword", + "doc-glossary", + "doc-glossref", + "doc-index", + "doc-introduction", + "doc-noteref", + "doc-notice", + "doc-pagebreak", + "doc-pagelist", + "doc-part", + "doc-preface", + "doc-prologue", + "doc-pullquote", + "doc-qna", + "doc-subtitle", + "doc-tip", + "doc-toc", + ]; + for (let attr of dpub_attrs) { + testAttrs(attr, {"xml-roles": attr}, true); + } + SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343537" + title="implement ARIA DPUB extension"> + Bug 1343537 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + <div id="doc-abstract" role="doc-abstract">abstract</div> + <div id="doc-acknowledgments" role="doc-acknowledgments">acknowledgments</div> + <div id="doc-afterword" role="doc-afterword">afterword</div> + <div id="doc-appendix" role="doc-appendix">appendix</div> + <div id="doc-backlink" role="doc-backlink">backlink</div> + <div id="doc-biblioentry" role="doc-biblioentry">biblioentry</div> + <div id="doc-bibliography" role="doc-bibliography">bibliography</div> + <div id="doc-biblioref" role="doc-biblioref">biblioref</div> + <div id="doc-chapter" role="doc-chapter">chapter</div> + <div id="doc-colophon" role="doc-colophon">colophon</div> + <div id="doc-conclusion" role="doc-conclusion">conclusion</div> + <div id="doc-cover" role="doc-cover">cover</div> + <div id="doc-credit" role="doc-credit">credit</div> + <div id="doc-credits" role="doc-credits">credits</div> + <div id="doc-dedication" role="doc-dedication">dedication</div> + <div id="doc-endnote" role="doc-endnote">endnote</div> + <div id="doc-endnotes" role="doc-endnotes">endnotes</div> + <div id="doc-epigraph" role="doc-epigraph">epigraph</div> + <div id="doc-epilogue" role="doc-epilogue">epilogue</div> + <div id="doc-errata" role="doc-errata">errata</div> + <div id="doc-example" role="doc-example">example</div> + <div id="doc-footnote" role="doc-footnote">footnote</div> + <div id="doc-foreword" role="doc-foreword">foreword</div> + <div id="doc-glossary" role="doc-glossary">glossary</div> + <div id="doc-glossref" role="doc-glossref">glossref</div> + <div id="doc-index" role="doc-index">index</div> + <div id="doc-introduction" role="doc-introduction">introduction</div> + <div id="doc-noteref" role="doc-noteref">noteref</div> + <div id="doc-notice" role="doc-notice">notice</div> + <div id="doc-pagebreak" role="doc-pagebreak">pagebreak</div> + <div id="doc-pagelist" role="doc-pagelist">pagelist</div> + <div id="doc-part" role="doc-part">part</div> + <div id="doc-preface" role="doc-preface">preface</div> + <div id="doc-prologue" role="doc-prologue">prologue</div> + <div id="doc-pullquote" role="doc-pullquote">pullquote</div> + <div id="doc-qna" role="doc-qna">qna</div> + <div id="doc-subtitle" role="doc-subtitle">subtitle</div> + <div id="doc-tip" role="doc-tip">tip</div> + <div id="doc-toc" role="doc-toc">toc</div> +</body> +</html> diff --git a/accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html b/accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html new file mode 100644 index 0000000000..45d5e2fa0b --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_graphics_aria_xml-roles.html @@ -0,0 +1,48 @@ +<!DOCTYPE html> +<html> +<head> + <title>XML roles tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + function doTest() { + // Graphics ARIA roles should be exposed via the xml-roles object attribute. + let graphics_attrs = [ + "graphics-document", + "graphics-object", + "graphics-symbol", + ]; + for (let attr of graphics_attrs) { + testAttrs(attr, {"xml-roles": attr}, true); + } + SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432513" + title="implement ARIA Graphics roles"> + Bug 1432513 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + <div id="graphics-document" role="graphics-document">document</div> + <div id="graphics-object" role="graphics-object">object</div> + <div id="graphics-symbol" role="graphics-symbol">symbol</div> +</body> +</html> diff --git a/accessible/tests/mochitest/attributes/test_listbox.html b/accessible/tests/mochitest/attributes/test_listbox.html new file mode 100644 index 0000000000..5489e74b74 --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_listbox.html @@ -0,0 +1,82 @@ +<html> + +<head> + <title>Listbox group attribute tests</title> + <meta charset="utf-8" /> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function doTest() { + // First test the whole lot. + testGroupAttrs("a", 1, 6); + testGroupAttrs("b", 2, 6); + testGroupAttrs("c", 3, 6); + testGroupAttrs("d", 4, 6); + testGroupAttrs("e", 5, 6); + testGroupAttrs("f", 6, 6); + // Remove c, reducing the set to 5. + let listbox = getAccessible("listbox"); + let updated = waitForEvent(EVENT_REORDER, listbox); + c.remove(); + await updated; + testGroupAttrs("a", 1, 5); + testGroupAttrs("b", 2, 5); + testGroupAttrs("d", 3, 5); + testGroupAttrs("e", 4, 5); + testGroupAttrs("f", 5, 5); + // Now, remove the first element. + updated = waitForEvent(EVENT_REORDER, listbox); + a.remove(); + await updated; + testGroupAttrs("b", 1, 4); + testGroupAttrs("d", 2, 4); + testGroupAttrs("e", 3, 4); + testGroupAttrs("f", 4, 4); + // Remove the last item. + updated = waitForEvent(EVENT_REORDER, listbox); + f.remove(); + await updated; + testGroupAttrs("b", 1, 3); + testGroupAttrs("d", 2, 3); + testGroupAttrs("e", 3, 3); + // Finally, remove the middle item. + updated = waitForEvent(EVENT_REORDER, listbox); + d.remove(); + await updated; + testGroupAttrs("b", 1, 2); + testGroupAttrs("e", 2, 2); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Group information updated after removal of list items, bug 1515186 --> + <div id="listbox" role="listbox"> + <div id="a" role="option">Option a</div> + <div id="b" role="option">Option b</div> + <div id="c" role="option">Option c</div> + <div id="d" role="option">Option d</div> + <div id="e" role="option">Option e</div> + <div id="f" role="option">Option f</div> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/attributes/test_obj.html b/accessible/tests/mochitest/attributes/test_obj.html new file mode 100644 index 0000000000..0d4d84a1b8 --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_obj.html @@ -0,0 +1,300 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=475006 +https://bugzilla.mozilla.org/show_bug.cgi?id=391829 +https://bugzilla.mozilla.org/show_bug.cgi?id=581952 +https://bugzilla.mozilla.org/show_bug.cgi?id=558036 +--> +<head> + <title>Group attributes tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + function doTest() { + // aria + testAttrs("atomic", {"atomic": "true", "container-atomic": "true"}, true); + testAttrs(getNode("atomic").firstChild, {"container-atomic": "true"}, true); + testAbsentAttrs("atomic_false", {"atomic": "false", "container-atomic": "false"}); + testAbsentAttrs(getNode("atomic_false").firstChild, {"container-atomic": "false"}); + + testAttrs("autocomplete", {"autocomplete": "true"}, true); + testAttrs("checkbox", {"checkable": "true"}, true); + testAttrs("checkedCheckbox", {"checkable": "true"}, true); + testAbsentAttrs("checkedMenuitem", {"checkable": "true"}, true); + testAttrs("checkedMenuitemCheckbox", {"checkable": "true"}, true); + testAttrs("checkedMenuitemRadio", {"checkable": "true"}, true); + testAttrs("checkedOption", {"checkable": "true"}, true); + testAttrs("checkedRadio", {"checkable": "true"}, true); + testAttrs("checkedTreeitem", {"checkable": "true"}, true); + testAttrs("dropeffect", {"dropeffect": "copy"}, true); + testAttrs("grabbed", {"grabbed": "true"}, true); + testAttrs("haspopupTrue", { "haspopup": "true" }, true); + testAbsentAttrs("haspopupFalse", { "haspopup": "false" }); + testAbsentAttrs("haspopupEmpty", { "haspopup": "" }); + testAttrs("haspopupDialog", { "haspopup": "dialog" }, true); + testAttrs("haspopupListbox", { "haspopup": "listbox" }, true); + testAttrs("haspopupMenu", { "haspopup": "menu" }, true); + testAttrs("haspopupTree", { "haspopup": "tree" }, true); + testAbsentAttrs("modal", {"modal": "true"}); + testAttrs("sortAscending", {"sort": "ascending"}, true); + testAttrs("sortDescending", {"sort": "descending"}, true); + testAttrs("sortNone", {"sort": "none"}, true); + testAttrs("sortOther", {"sort": "other"}, true); + testAttrs("roledescr", {"roledescription": "spreadshit"}, true); + testAttrs("currentPage", {"current": "page"}, true); + testAttrs("currentStep", {"current": "step"}, true); + testAttrs("currentLocation", {"current": "location"}, true); + testAttrs("currentDate", {"current": "date"}, true); + testAttrs("currentTime", {"current": "time"}, true); + testAttrs("currentTrue", {"current": "true"}, true); + testAttrs("currentOther", {"current": "true"}, true); + testAbsentAttrs("currentFalse", {"current": "true"}); + testAttrs("currentSpan", {"current": "page"}, true); + + // inherited attributes by subdocuments + var subdoc = getAccessible("iframe").firstChild; + testAttrs(subdoc, {"busy": "true"}, true); + + // live object attribute + + // HTML + testAttrs("output", {"live": "polite"}, true); + + // ARIA + testAttrs("live", {"live": "polite"}, true); + testAttrs("live2", {"live": "polite"}, true); + testAbsentAttrs("live3", {"live": ""}); + if (MAC) { + testAttrs("alert", {"live": "assertive"}, true); + } else { + testAbsentAttrs("alert", {"live": "assertive"}); + } + testAttrs("log", {"live": "polite"}, true); + testAttrs("logAssertive", {"live": "assertive"}, true); + testAttrs("marquee", {"live": "off"}, true); + testAttrs("status", {"live": "polite"}, true); + testAttrs("timer", {"live": "off"}, true); + testAbsentAttrs("tablist", {"live": "polite"}); + + // container-live object attribute + testAttrs("liveChild", {"container-live": "polite"}, true); + testAttrs("live2Child", {"container-live": "polite"}, true); + if (MAC) { + testAttrs("alertChild", {"container-live": "assertive"}, true); + } else { + testAbsentAttrs("alertChild", {"container-live": "assertive"}); + } + testAttrs("logChild", {"container-live": "polite"}, true); + testAttrs("logAssertiveChild", {"container-live": "assertive"}, true); + testAttrs("marqueeChild", {"container-live": "off"}, true); + testAttrs("statusChild", {"container-live": "polite"}, true); + testAttrs("timerChild", {"container-live": "off"}, true); + testAbsentAttrs("tablistChild", {"container-live": "polite"}); + testAttrs("containerLiveOutput", {"container-live": "polite"}, true); + testAttrs("containerLiveOutput1", {"container-live": "polite"}, true); + testAttrs("containerLiveOutput2", {"container-live": "polite"}, true); + + // container-live-role object attribute + testAttrs("log", {"container-live-role": "log"}, true); + testAttrs("logAssertive", {"container-live-role": "log"}, true); + testAttrs("marquee", {"container-live-role": "marquee"}, true); + testAttrs("status", {"container-live-role": "status"}, true); + testAttrs("timer", {"container-live-role": "timer"}, true); + testAttrs("logChild", {"container-live-role": "log"}, true); + testAttrs("logAssertive", {"container-live-role": "log"}, true); + testAttrs("logAssertiveChild", {"container-live-role": "log"}, true); + testAttrs("marqueeChild", {"container-live-role": "marquee"}, true); + testAttrs("statusChild", {"container-live-role": "status"}, true); + testAttrs("timerChild", {"container-live-role": "timer"}, true); + testAbsentAttrs("tablistChild", {"container-live-role": "tablist"}); + + // absent aria-label and aria-labelledby object attribute + testAbsentAttrs("label", {"label": "foo"}); + testAbsentAttrs("labelledby", {"labelledby": "label"}); + + // container that has no default live attribute + testAttrs("liveGroup", {"live": "polite"}, true); + testAttrs("liveGroupChild", {"container-live": "polite"}, true); + testAttrs("liveGroup", {"container-live-role": "group"}, true); + testAttrs("liveGroupChild", {"container-live-role": "group"}, true); + + // text input type + testAbsentAttrs("button", { "text-input-type": "button"}); + testAbsentAttrs("checkbox", { "text-input-type": "checkbox"}); + testAbsentAttrs("radio", { "text-input-type": "radio"}); + testAttrs("email", {"text-input-type": "email"}, true); + testAttrs("search", {"text-input-type": "search"}, true); + testAttrs("tel", {"text-input-type": "tel"}, true); + testAttrs("url", {"text-input-type": "url"}, true); + testAttrs("number", {"text-input-type": "number"}, true); + + // ARIA + testAttrs("searchbox", {"text-input-type": "search"}, true); + + // html + testAttrs("radio", {"checkable": "true"}, true); + testAttrs("checkbox", {"checkable": "true"}, true); + testAttrs("draggable", {"draggable": "true"}, true); + testAttrs("th1", { "abbr": "SS#" }, true); + testAttrs("th2", { "abbr": "SS#" }, true); + testAttrs("th2", { "axis": "social" }, true); + + // don't barf on an empty abbr element. + testAbsentAttrs("th3", { "abbr": "" }, true); + + // application accessible + if (WIN) { + var gfxInfo = Cc["@mozilla.org/gfx/info;1"]. + getService(Ci.nsIGfxInfo); + var attrs = { + "D2D": (gfxInfo.D2DEnabled ? "true" : "false"), + }; + testAttrs(getApplicationAccessible(), attrs, false); + } + + // no object attributes + testAbsentAttrs(getAccessible("listitem").firstChild, { "tag": "" }); + + // experimental aria + testAttrs("experimental", {"blah": "true"}, true); + + // HTML5 aside element xml-roles + testAttrs("aside0", {"xml-roles": "note"}, true); + testAttrs("aside1", {"xml-roles": "group"}, true); + testAttrs("aside2", {"xml-roles": "complementary"}, true); + + // non-standard data-at-shortcutkeys attribute: + testAttrs("shortcuts", {'data-at-shortcutkeys': '{"n":"New message","r":"Reply to message"}'}, true); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- container live --> + <output id="containerLiveOutput"><div id="containerLiveOutput1"><div id="containerLiveOutput2">Test</div></div></output> + + <!-- aria --> + <div id="atomic" aria-atomic="true">live region</div> + <div id="atomic_false" aria-atomic="false">live region</div> + <div id="autocomplete" role="textbox" aria-autocomplete="true"></div> + <div id="checkbox" role="checkbox"></div> + <div id="checkedCheckbox" role="checkbox" aria-checked="true"></div> + <div id="checkedMenuitem" role="menuitem" aria-checked="true"></div> + <div id="checkedMenuitemCheckbox" role="menuitemcheckbox" aria-checked="true"></div> + <div id="checkedMenuitemRadio" role="menuitemradio" aria-checked="true"></div> + <div id="checkedOption" role="option" aria-checked="true"></div> + <div id="checkedRadio" role="radio" aria-checked="true"></div> + <div id="checkedTreeitem" role="treeitem" aria-checked="true"></div> + <div id="dropeffect" aria-dropeffect="copy"></div> + <div id="grabbed" aria-grabbed="true"></div> + <div id="haspopupTrue" aria-haspopup="true"></div> + <div id="haspopupFalse" aria-haspopup="false"></div> + <div id="haspopupEmpty" aria-haspopup=""></div> + <div id="haspopupDialog" aria-haspopup="dialog"></div> + <div id="haspopupListbox" aria-haspopup="listbox"></div> + <div id="haspopupMenu" aria-haspopup="menu"></div> + <div id="haspopupTree" aria-haspopup="tree"></div> + <div id="modal" aria-modal="true"></div> + <div id="sortAscending" role="columnheader" aria-sort="ascending"></div> + <div id="sortDescending" role="columnheader" aria-sort="descending"></div> + <div id="sortNone" role="columnheader" aria-sort="none"></div> + <div id="sortOther" role="columnheader" aria-sort="other"></div> + <div id="roledescr" aria-roledescription="spreadshit"></div> + <div id="currentPage" aria-current="page"></div> + <div id="currentStep" aria-current="step"></div> + <div id="currentLocation" aria-current="location"></div> + <div id="currentDate" aria-current="date"></div> + <div id="currentTime" aria-current="time"></div> + <div id="currentTrue" aria-current="true"></div> + <div id="currentOther" aria-current="other"></div> + <div id="currentFalse" aria-current="false"></div> + + <!-- aria-current on a span which must create an accessible --> + <ol> + <li><a href="...">Page 1</a></li> + <li><a href="...">Page 2</a></li> + <li><span id="currentSpan" aria-current="page">This page</span></li> + </ol> + + <!-- inherited from iframe --> + <iframe id="iframe" src="data:text/html,<html><body></body></html>" + aria-busy="true"></iframe> + + <!-- html --> + <output id="output"></output> + + <!-- back to aria --> + <div id="live" aria-live="polite">excuse <div id="liveChild">me</div></div> + <div id="live2" role="marquee" aria-live="polite">excuse <div id="live2Child">me</div></div> + <div id="live3" role="region">excuse</div> + <div id="alert" role="alert">excuse <div id="alertChild">me</div></div> + <div id="log" role="log">excuse <div id="logChild">me</div></div> + <div id="logAssertive" role="log" aria-live="assertive">excuse <div id="logAssertiveChild">me</div></div> + <div id="marquee" role="marquee">excuse <div id="marqueeChild">me</div></div> + <div id="status" role="status">excuse <div id="statusChild">me</div></div> + <div id="tablist" role="tablist">tablist <div id="tablistChild">tab</div></div> + <div id="timer" role="timer">excuse <div id="timerChild">me</div></div> + + <!-- aria-label[ledby] should not be an object attribute --> + <div id="label" role="checkbox" aria-label="foo"></div> + <div id="labelledby" role="checkbox" aria-labelledby="label"></div> + + <!-- unusual live case --> + <div id="liveGroup" role="group" aria-live="polite"> + excuse <div id="liveGroupChild">me</div> + </div> + + <!-- text input type --> + <input id="button" type="button"/> + <input id="email" type="email"/> + <input id="search" type="search"/> + <input id="tel" type="tel"/> + <input id="url" type="url"/> + <input id="number" type="number"/> + <div id="searchbox" role="searchbox"></div> + + <!-- html --> + <input id="radio" type="radio"/> + <input id="checkbox" type="checkbox"/> + <div id="draggable" draggable="true">Draggable div</div> + <table> + <tr> + <th id="th1"><abbr title="Social Security Number">SS#</abbr></th> + <th id="th2" abbr="SS#" axis="social">Social Security Number</th> + <th id="th3"><abbr></abbr></th> + </tr> + </table> + + <ul> + <li id="listitem">item + </ul> + + <!-- experimental aria --> + <div id="experimental" aria-blah="true">Fake beer</div> + + <!-- HTML5 aside elements --> + <aside id="aside0" role="note">aside 0</aside> + <aside id="aside1" role="group">aside 1</aside> + <aside id="aside2">aside 2</aside> + + <!-- Shortcuts for web applications --> + <div id="shortcuts" data-at-shortcutkeys='{"n":"New message","r":"Reply to message"}'></div> +</body> +</html> diff --git a/accessible/tests/mochitest/attributes/test_obj_css.html b/accessible/tests/mochitest/attributes/test_obj_css.html new file mode 100644 index 0000000000..6c702ba5a6 --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_obj_css.html @@ -0,0 +1,225 @@ +<html> +<head> + <title>CSS-like attributes tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + var gQueue = null; + + function removeElm(aID) { + this.node = getNode(aID); + this.accessible = getAccessible(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.accessible), + ]; + + this.invoke = function removeElm_invoke() { + this.node.remove(); + }; + + this.check = function removeElm_check() { + testAbsentCSSAttrs(this.accessible); + }; + + this.getID = function removeElm_getID() { + return "test CSS-based attributes on removed accessible"; + }; + } + + function doTest() { + // CSS display + testCSSAttrs("display_block"); + testCSSAttrs("display_inline"); + testCSSAttrs("display_inline-block"); + testCSSAttrs("display_list-item"); + testCSSAttrs("display_table"); + testCSSAttrs("display_inline-table"); + testCSSAttrs("display_table-row-group"); + testCSSAttrs("display_table-column"); + testCSSAttrs("display_table-column-group"); + testCSSAttrs("display_table-header-group"); + testCSSAttrs("display_table-footer-group"); + testCSSAttrs("display_table-row"); + testCSSAttrs("display_table-cell"); + testCSSAttrs("display_table-caption"); + + // CSS text-align + testCSSAttrs("text-align_left"); + testCSSAttrs("text-align_right"); + testCSSAttrs("text-align_center"); + testCSSAttrs("text-align_justify"); + testCSSAttrs("text-align_inherit"); + + // CSS text-indent + testCSSAttrs("text-indent_em"); + testCSSAttrs("text-indent_ex"); + testCSSAttrs("text-indent_in"); + testCSSAttrs("text-indent_cm"); + testCSSAttrs("text-indent_mm"); + testCSSAttrs("text-indent_pt"); + testCSSAttrs("text-indent_pc"); + testCSSAttrs("text-indent_px"); + testCSSAttrs("text-indent_percent"); + testCSSAttrs("text-indent_inherit"); + + // CSS margin + testCSSAttrs("margin_em"); + testCSSAttrs("margin_ex"); + testCSSAttrs("margin_in"); + testCSSAttrs("margin_cm"); + testCSSAttrs("margin_mm"); + testCSSAttrs("margin_pt"); + testCSSAttrs("margin_pc"); + testCSSAttrs("margin_px"); + testCSSAttrs("margin_percent"); + testCSSAttrs("margin_auto"); + testCSSAttrs("margin_inherit"); + + testCSSAttrs("margin-left"); + testCSSAttrs("margin-right"); + testCSSAttrs("margin-top"); + testCSSAttrs("margin-bottom"); + + // Elements + testCSSAttrs("span"); + testCSSAttrs("div"); + testCSSAttrs("p"); + testCSSAttrs("input"); + testCSSAttrs("table"); + testCSSAttrs("tr"); + testCSSAttrs("td"); + + // no CSS-based object attributes + testAbsentCSSAttrs(getAccessible("listitem").firstChild); + + gQueue = new eventQueue(); + gQueue.push(new removeElm("div")); + gQueue.invoke(); // SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=439566" + title="Include the css display property as an IAccessible2 object attribute"> + Mozilla Bug 439566 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=460932" + title="text-indent and text-align should really be object attribute"> + Mozilla Bug 460932 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=689540" + title="Expose IA2 margin- object attributes"> + Mozilla Bug 689540 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=714579" + title="Don't use GetComputedStyle for object attribute calculation"> + Mozilla Bug 714579 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=729831" + title="Don't expose CSS-based object attributes on not in tree accessible and accessible having no DOM element"> + Mozilla Bug 729831 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="display_block" role="img" + style="display: block;">display: block</div> + <div id="display_inline" role="img" + style="display: inline;">display: inline</div> + <div id="display_inline-block" role="img" + style="display: inline-block;">display: inline-block</div> + <div id="display_list-item" role="img" + style="display: list-item;">display: list-item</div> + <div id="display_table" role="img" + style="display: table;">display: table</div> + <div id="display_inline-table" role="img" + style="display: inline-table;">display: inline-table</div> + <div id="display_table-row-group" role="img" + style="display: table-row-group;">display: table-row-group</div> + <div id="display_table-column" role="img" + style="display: table-column;">display: table-column</div> + <div id="display_table-column-group" role="img" + style="display: table-column-group;">display: table-column-group</div> + <div id="display_table-header-group" role="img" + style="display: table-header-group;">display: table-header-group</div> + <div id="display_table-footer-group" role="img" + style="display: table-footer-group;">display: table-footer-group</div> + <div id="display_table-row" role="img" + style="display: table-row;">display: table-row</div> + <div id="display_table-cell" role="img" + style="display: table-cell;">display: table-cell</div> + <div id="display_table-caption" role="img" + style="display: table-caption;">display: table-caption</div> + + <p id="text-align_left" style="text-align: left;">text-align: left</p> + <p id="text-align_right" style="text-align: right;">text-align: right</p> + <p id="text-align_center" style="text-align: center;">text-align: center</p> + <p id="text-align_justify" style="text-align: justify;">text-align: justify</p> + <p id="text-align_inherit" style="text-align: inherit;">text-align: inherit</p> + + <p id="text-indent_em" style="text-indent: 0.5em;">text-indent: 0.5em</p> + <p id="text-indent_ex" style="text-indent: 1ex;">text-indent: 1ex</p> + <p id="text-indent_in" style="text-indent: 0.5in;">text-indent: 0.5in</p> + <p id="text-indent_cm" style="text-indent: 2cm;">text-indent: 2cm</p> + <p id="text-indent_mm" style="text-indent: 10mm;">text-indent: 10mm</p> + <p id="text-indent_pt" style="text-indent: 30pt;">text-indent: 30pt</p> + <p id="text-indent_pc" style="text-indent: 2pc;">text-indent: 2pc</p> + <p id="text-indent_px" style="text-indent: 5px;">text-indent: 5px</p> + <p id="text-indent_percent" style="text-indent: 10%;">text-indent: 10%</p> + <p id="text-indent_inherit" style="text-indent: inherit;">text-indent: inherit</p> + + <p id="margin_em" style="margin: 0.5em;">margin: 0.5em</p> + <p id="margin_ex" style="margin: 1ex;">margin: 1ex</p> + <p id="margin_in" style="margin: 0.5in;">margin: 0.5in</p> + <p id="margin_cm" style="margin: 2cm;">margin: 2cm</p> + <p id="margin_mm" style="margin: 10mm;">margin: 10mm</p> + <p id="margin_pt" style="margin: 30pt;">margin: 30pt</p> + <p id="margin_pc" style="margin: 2pc;">margin: 2pc</p> + <p id="margin_px" style="margin: 5px;">margin: 5px</p> + <p id="margin_percent" style="margin: 10%;">margin: 10%</p> + <p id="margin_auto" style="margin: auto;">margin: auto</p> + <p id="margin_inherit" style="margin: inherit;">margin: inherit</p> + + <p id="margin-left" style="margin-left: 11px;">margin-left: 11px</p> + <p id="margin-right" style="margin-right: 21px;">margin-right</p> + <p id="margin-top" style="margin-top: 31px;">margin-top: 31px</p> + <p id="margin-bottom" style="margin-bottom: 41px;">margin-bottom: 41px</p> + + <span id="span" role="group">It's span</span> + <div id="div">It's div</div> + <p id="p">It's paragraph"</p> + <input id="input"/> + <table id="table" style="margin: 2px; text-align: center; text-indent: 10%;"> + <tr id="tr" role="group"> + <td id="td">td</td> + </tr> + </table> + + <ul> + <li id="listitem">item + </ul> +</body> +</html> diff --git a/accessible/tests/mochitest/attributes/test_obj_css.xhtml b/accessible/tests/mochitest/attributes/test_obj_css.xhtml new file mode 100644 index 0000000000..df9afceba0 --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_obj_css.xhtml @@ -0,0 +1,57 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessibility CSS-based Object Attributes Test."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../attributes.js" /> + + <script type="application/javascript"> + <![CDATA[ + function doTest() + { + // CSS display + testCSSAttrs("display_mozbox"); + testCSSAttrs("display_mozinlinebox"); + testCSSAttrs("display_mozdeck"); + testCSSAttrs("display_mozpopup"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=714579" + title="Don't use GetComputedStyle for object attribute calculation"> + Mozilla Bug 714579 + </a><br/> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="display_mozbox" style="display: -moz-box;" role="img"/> + <vbox id="display_mozinlinebox" style="display: -moz-inline-box;" role="img"/> + <vbox id="display_mozdeck" style="display: -moz-deck;" role="img"/> + <vbox id="display_mozpopup" style="display: -moz-popup;" role="img"/> + + </hbox> +</window> diff --git a/accessible/tests/mochitest/attributes/test_obj_group.html b/accessible/tests/mochitest/attributes/test_obj_group.html new file mode 100644 index 0000000000..6bfaaf7285 --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_obj_group.html @@ -0,0 +1,566 @@ +<html> + +<head> + <title>Group attributes tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // HTML select with no size attribute. + testGroupAttrs("opt1-nosize", 1, 4); + testGroupAttrs("opt2-nosize", 2, 4); + testGroupAttrs("opt3-nosize", 3, 4); + testGroupAttrs("opt4-nosize", 4, 4); + + // Container should have item count and not hierarchical + testGroupParentAttrs(getAccessible("opt1-nosize").parent, 4, false); + + // //////////////////////////////////////////////////////////////////////// + // HTML select + testGroupAttrs("opt1", 1, 2); + testGroupAttrs("opt2", 2, 2); + + // //////////////////////////////////////////////////////////////////////// + // HTML select with options + // XXX bug 469123 + // testGroupAttrs("select2_optgroup", 1, 3, 1); + // testGroupAttrs("select2_opt3", 2, 3, 1); + // testGroupAttrs("select2_opt4", 3, 3, 1); + // testGroupAttrs("select2_opt1", 1, 2, 2); + // testGroupAttrs("select2_opt2", 2, 2, 2); + + // //////////////////////////////////////////////////////////////////////// + // HTML input@type="radio" within form + testGroupAttrs("radio1", 1, 2); + testGroupAttrs("radio2", 2, 2); + + // //////////////////////////////////////////////////////////////////////// + // HTML input@type="radio" within document + testGroupAttrs("radio3", 1, 2); + testGroupAttrs("radio4", 2, 2); + + // //////////////////////////////////////////////////////////////////////// + // Hidden HTML input@type="radio" + testGroupAttrs("radio5", 1, 1); + + // //////////////////////////////////////////////////////////////////////// + // HTML ul/ol + testGroupAttrs("li1", 1, 3); + testGroupAttrs("li2", 2, 3); + testGroupAttrs("li3", 3, 3); + + // ul should have item count and not hierarchical + testGroupParentAttrs("ul", 3, false); + + // //////////////////////////////////////////////////////////////////////// + // HTML ul/ol (nested lists) + + testGroupAttrs("li4", 1, 3, 1); + testGroupAttrs("li5", 2, 3, 1); + testGroupAttrs("li6", 3, 3, 1); + // ol with nested list should have 1st level item count and be hierarchical + testGroupParentAttrs("ol", 3, true); + + testGroupAttrs("n_li4", 1, 3, 2); + testGroupAttrs("n_li5", 2, 3, 2); + testGroupAttrs("n_li6", 3, 3, 2); + // nested ol should have item count and be hierarchical + testGroupParentAttrs("ol_nested", 3, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA list + testGroupAttrs("li7", 1, 3); + testGroupAttrs("li8", 2, 3); + testGroupAttrs("li9", 3, 3); + // simple flat aria list + testGroupParentAttrs("aria-list_1", 3, false); + + // //////////////////////////////////////////////////////////////////////// + // ARIA list (nested lists: list -> listitem -> list -> listitem) + testGroupAttrs("li10", 1, 3, 1); + testGroupAttrs("li11", 2, 3, 1); + testGroupAttrs("li12", 3, 3, 1); + // aria list with nested list + testGroupParentAttrs("aria-list_2", 3, true); + + testGroupAttrs("n_li10", 1, 3, 2); + testGroupAttrs("n_li11", 2, 3, 2); + testGroupAttrs("n_li12", 3, 3, 2); + // nested aria list. + testGroupParentAttrs("aria-list_2_1", 3, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA list (nested lists: list -> listitem -> group -> listitem) + testGroupAttrs("lgt_li1", 1, 2, 1); + testGroupAttrs("lgt_li1_nli1", 1, 2, 2); + testGroupAttrs("lgt_li1_nli2", 2, 2, 2); + testGroupAttrs("lgt_li2", 2, 2, 1); + testGroupAttrs("lgt_li2_nli1", 1, 2, 2); + testGroupAttrs("lgt_li2_nli2", 2, 2, 2); + // aria list with nested list + testGroupParentAttrs("aria-list_3", 2, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA menu (menuitem, separator, menuitemradio and menuitemcheckbox) + testGroupAttrs("menu_item1", 1, 2); + testGroupAttrs("menu_item2", 2, 2); + testGroupAttrs("menu_item1.1", 1, 2); + testGroupAttrs("menu_item1.2", 2, 2); + testGroupAttrs("menu_item1.3", 1, 3); + testGroupAttrs("menu_item1.4", 2, 3); + testGroupAttrs("menu_item1.5", 3, 3); + // menu bar item count + testGroupParentAttrs("menubar", 2, false); + // Bug 1492529. Menu should have total number of items 5 from both sets, + // but only has the first 2 item set. + todoAttr("menu", "child-item-count", "5"); + + // //////////////////////////////////////////////////////////////////////// + // ARIA tab + testGroupAttrs("tab_1", 1, 3); + testGroupAttrs("tab_2", 2, 3); + testGroupAttrs("tab_3", 3, 3); + // tab list tab count + testGroupParentAttrs("tablist_1", 3, false); + + // //////////////////////////////////////////////////////////////////////// + // ARIA radio + testGroupAttrs("r1", 1, 3); + testGroupAttrs("r2", 2, 3); + testGroupAttrs("r3", 3, 3); + // explicit aria radio group + testGroupParentAttrs("rg1", 3, false); + + // //////////////////////////////////////////////////////////////////////// + // ARIA tree + testGroupAttrs("ti1", 1, 3, 1); + testGroupAttrs("ti2", 1, 2, 2); + testGroupAttrs("ti3", 2, 2, 2); + testGroupAttrs("ti4", 2, 3, 1); + testGroupAttrs("ti5", 1, 3, 2); + testGroupAttrs("ti6", 2, 3, 2); + testGroupAttrs("ti7", 3, 3, 2); + testGroupAttrs("ti8", 3, 3, 1); + testGroupParentAttrs("tree_1", 3, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA tree (tree -> treeitem -> group -> treeitem) + testGroupAttrs("tree2_ti1", 1, 2, 1); + testGroupAttrs("tree2_ti1a", 1, 2, 2); + testGroupAttrs("tree2_ti1b", 2, 2, 2); + testGroupAttrs("tree2_ti2", 2, 2, 1); + testGroupAttrs("tree2_ti2a", 1, 2, 2); + testGroupAttrs("tree2_ti2b", 2, 2, 2); + testGroupParentAttrs("tree_2", 2, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA tree (tree -> treeitem, group -> treeitem) + testGroupAttrs("tree3_ti1", 1, 2, 1); + testGroupAttrs("tree3_ti1a", 1, 2, 2); + testGroupAttrs("tree3_ti1b", 2, 2, 2); + testGroupAttrs("tree3_ti2", 2, 2, 1); + testGroupAttrs("tree3_ti2a", 1, 2, 2); + testGroupAttrs("tree3_ti2b", 2, 2, 2); + testGroupParentAttrs("tree_3", 2, true); + + // //////////////////////////////////////////////////////////////////////// + // ARIA grid + testGroupAttrs("grid_row1", 1, 2); + testAbsentAttrs("grid_cell1", {"posinset": "", "setsize": ""}); + testAbsentAttrs("grid_cell2", {"posinset": "", "setsize": ""}); + + testGroupAttrs("grid_row2", 2, 2); + testAbsentAttrs("grid_cell3", {"posinset": "", "setsize": ""}); + testAbsentAttrs("grid_cell4", {"posinset": "", "setsize": ""}); + testGroupParentAttrs("grid", 2, false); + + // //////////////////////////////////////////////////////////////////////// + // ARIA treegrid + testGroupAttrs("treegrid_row1", 1, 2, 1); + testAbsentAttrs("treegrid_cell1", {"posinset": "", "setsize": ""}); + testAbsentAttrs("treegrid_cell2", {"posinset": "", "setsize": ""}); + + testGroupAttrs("treegrid_row2", 1, 1, 2); + testAbsentAttrs("treegrid_cell3", {"posinset": "", "setsize": ""}); + testAbsentAttrs("treegrid_cell4", {"posinset": "", "setsize": ""}); + + testGroupAttrs("treegrid_row3", 2, 2, 1); + testAbsentAttrs("treegrid_cell5", {"posinset": "", "setsize": ""}); + testAbsentAttrs("treegrid_cell6", {"posinset": "", "setsize": ""}); + + testGroupParentAttrs("treegrid", 2, true); + // row child item count provided by parent grid's aria-colcount + testGroupParentAttrs("treegrid_row1", 4, false); + + // //////////////////////////////////////////////////////////////////////// + // HTML headings + testGroupAttrs("h1", 0, 0, 1); + testGroupAttrs("h2", 0, 0, 2); + testGroupAttrs("h3", 0, 0, 3); + testGroupAttrs("h4", 0, 0, 4); + testGroupAttrs("h5", 0, 0, 5); + testGroupAttrs("h6", 0, 0, 6); + testGroupAttrs("ariaHeadingNoLevel", 0, 0, 2); + // No child item counts or "hierarchical" flag for parent of headings + testAbsentAttrs("headings", {"child-item-count": "", "hierarchical": ""}); + + // //////////////////////////////////////////////////////////////////////// + // ARIA combobox + testGroupAttrs("combo1_opt1", 1, 4); + testGroupAttrs("combo1_opt2", 2, 4); + testGroupAttrs("combo1_opt3", 3, 4); + testGroupAttrs("combo1_opt4", 4, 4); + testGroupParentAttrs("combo1", 4, false); + + // //////////////////////////////////////////////////////////////////////// + // ARIA table + testGroupAttrs("table_cell", 3, 4); + testGroupAttrs("table_row", 2, 2); + + // grid child item count provided by aria-rowcount + testGroupParentAttrs("table", 2, false); + // row child item count provided by parent grid's aria-colcount + testGroupParentAttrs("table_row", 4, false); + + // Attributes calculated even when row is wrapped in a div. + testGroupAttrs("wrapped_row_1", 1, 2); + testGroupAttrs("wrapped_row_2", 2, 2); + + // //////////////////////////////////////////////////////////////////////// + // ARIA list constructed by ARIA owns + testGroupAttrs("t1_li1", 1, 3); + testGroupAttrs("t1_li2", 2, 3); + testGroupAttrs("t1_li3", 3, 3); + testGroupParentAttrs("aria-list_4", 3, false); + + // Test group attributes of ARIA comments + testGroupAttrs("comm_single_1", 1, 2, 1); + testGroupAttrs("comm_single_2", 2, 2, 1); + testGroupAttrs("comm_nested_1", 1, 3, 1); + testGroupAttrs("comm_nested_1_1", 1, 2, 2); + testGroupAttrs("comm_nested_1_2", 2, 2, 2); + testGroupAttrs("comm_nested_2", 2, 3, 1); + testGroupAttrs("comm_nested_2_1", 1, 1, 2); + testGroupAttrs("comm_nested_2_1_1", 1, 1, 3); + testGroupAttrs("comm_nested_3", 3, 3, 1); + + // Test that group position information updates after deleting node. + testGroupAttrs("tree4_ti1", 1, 2, 1); + testGroupAttrs("tree4_ti2", 2, 2, 1); + testGroupParentAttrs("tree4", 2, true); + + var tree4element = document.getElementById("tree4_ti1"); + var tree4acc = getAccessible("tree4"); + tree4element.remove(); + waitForEvent(EVENT_REORDER, tree4acc, function() { + testGroupAttrs("tree4_ti2", 1, 1, 1); + testGroupParentAttrs("tree4", 1, true); + SimpleTest.finish(); + }); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=468418" + title="Expose level for nested lists in HTML"> + Mozilla Bug 468418 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=844023" + title="group info might not be properly updated when flat trees mutate"> + Bug 844023 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=864224" + title="Support nested ARIA listitems structured by role='group'"> + Bug 864224 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=907682" + title=" HTML:option group position is not correct when select is collapsed"> + Mozilla Bug 907682 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select> + <option id="opt1-nosize">option1</option> + <option id="opt2-nosize">option2</option> + <option id="opt3-nosize">option3</option> + <option id="opt4-nosize">option4</option> + </select> + + <select size="4"> + <option id="opt1">option1</option> + <option id="opt2">option2</option> + </select> + + <select size="4"> + <optgroup id="select2_optgroup" label="group"> + <option id="select2_opt1">option1</option> + <option id="select2_opt2">option2</option> + </optgroup> + <option id="select2_opt3">option3</option> + <option id="select2_opt4">option4</option> + </select> + + <form> + <input type="radio" id="radio1" name="group1"/> + <input type="radio" id="radio2" name="group1"/> + </form> + + <input type="radio" id="radio3" name="group2"/> + <input type="radio" id="radio4" name="group2"/> + + <ul id="ul"> + <li id="li1">Oranges</li> + <li id="li2">Apples</li> + <li id="li3">Bananas</li> + </ul> + + <ol id="ol"> + <li id="li4">Oranges</li> + <li id="li5">Apples</li> + <li id="li6">Bananas + <ul id="ol_nested"> + <li id="n_li4">Oranges</li> + <li id="n_li5">Apples</li> + <li id="n_li6">Bananas</li> + </ul> + </li> + </ol> + + <span role="list" id="aria-list_1"> + <span role="listitem" id="li7">Oranges</span> + <span role="listitem" id="li8">Apples</span> + <span role="listitem" id="li9">Bananas</span> + </span> + + <span role="list" id="aria-list_2"> + <span role="listitem" id="li10">Oranges</span> + <span role="listitem" id="li11">Apples</span> + <span role="listitem" id="li12">Bananas + <span role="list" id="aria-list_2_1"> + <span role="listitem" id="n_li10">Oranges</span> + <span role="listitem" id="n_li11">Apples</span> + <span role="listitem" id="n_li12">Bananas</span> + </span> + </span> + </span> + + <div role="list" id="aria-list_3"> + <div role="listitem" id="lgt_li1">Item 1 + <div role="group"> + <div role="listitem" id="lgt_li1_nli1">Item 1A</div> + <div role="listitem" id="lgt_li1_nli2">Item 1B</div> + </div> + </div> + <div role="listitem" id="lgt_li2">Item 2 + <div role="group"> + <div role="listitem" id="lgt_li2_nli1">Item 2A</div> + <div role="listitem" id="lgt_li2_nli2">Item 2B</div> + </div> + </div> + </div> + + <ul role="menubar" id="menubar"> + <li role="menuitem" aria-haspopup="true" id="menu_item1">File + <ul role="menu" id="menu"> + <li role="menuitem" id="menu_item1.1">New</li> + <li role="menuitem" id="menu_item1.2">Open…</li> + <li role="separator">-----</li> + <li role="menuitem" id="menu_item1.3">Item</li> + <li role="menuitemradio" id="menu_item1.4">Radio</li> + <li role="menuitemcheckbox" id="menu_item1.5">Checkbox</li> + </ul> + </li> + <li role="menuitem" aria-haspopup="false" id="menu_item2">Help</li> + </ul> + + <ul id="tablist_1" role="tablist"> + <li id="tab_1" role="tab">Crust</li> + <li id="tab_2" role="tab">Veges</li> + <li id="tab_3" role="tab">Carnivore</li> + </ul> + + <ul id="rg1" role="radiogroup"> + <li id="r1" role="radio" aria-checked="false">Thai</li> + <li id="r2" role="radio" aria-checked="false">Subway</li> + <li id="r3" role="radio" aria-checked="false">Jimmy Johns</li> + </ul> + + <table role="tree" id="tree_1"> + <tr role="presentation"> + <td role="treeitem" aria-expanded="true" aria-level="1" + id="ti1">vegetables</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti2">cucumber</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti3">carrot</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-expanded="false" aria-level="1" + id="ti4">cars</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti5">mercedes</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti6">BMW</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="2" id="ti7">Audi</td> + </tr> + <tr role="presentation"> + <td role="treeitem" aria-level="1" id="ti8">people</td> + </tr> + </table> + + <ul role="tree" id="tree_2"> + <li role="treeitem" id="tree2_ti1">Item 1 + <ul role="group"> + <li role="treeitem" id="tree2_ti1a">Item 1A</li> + <li role="treeitem" id="tree2_ti1b">Item 1B</li> + </ul> + </li> + <li role="treeitem" id="tree2_ti2">Item 2 + <ul role="group"> + <li role="treeitem" id="tree2_ti2a">Item 2A</li> + <li role="treeitem" id="tree2_ti2b">Item 2B</li> + </ul> + </li> + </div> + + <div role="tree" id="tree_3"> + <div role="treeitem" id="tree3_ti1">Item 1</div> + <div role="group"> + <li role="treeitem" id="tree3_ti1a">Item 1A</li> + <li role="treeitem" id="tree3_ti1b">Item 1B</li> + </div> + <div role="treeitem" id="tree3_ti2">Item 2</div> + <div role="group"> + <div role="treeitem" id="tree3_ti2a">Item 2A</div> + <div role="treeitem" id="tree3_ti2b">Item 2B</div> + </div> + </div> + + <!-- IMPORTANT: Need to have no whitespace between elements in this tree. --> + <div role="tree" id="tree4"><div role="treeitem" + id="tree4_ti1">Item 1</div><div role="treeitem" + id="tree4_ti2">Item 2</div></div> + + <table role="grid" id="grid"> + <tr role="row" id="grid_row1"> + <td role="gridcell" id="grid_cell1">cell1</td> + <td role="gridcell" id="grid_cell2">cell2</td> + </tr> + <tr role="row" id="grid_row2"> + <td role="gridcell" id="grid_cell3">cell3</td> + <td role="gridcell" id="grid_cell4">cell4</td> + </tr> + </table> + + <div role="treegrid" id="treegrid" aria-colcount="4"> + <div role="row" aria-level="1" id="treegrid_row1"> + <div role="gridcell" id="treegrid_cell1">cell1</div> + <div role="gridcell" id="treegrid_cell2">cell2</div> + </div> + <div role="row" aria-level="2" id="treegrid_row2"> + <div role="gridcell" id="treegrid_cell3">cell1</div> + <div role="gridcell" id="treegrid_cell4">cell2</div> + </div> + <div role="row" id="treegrid_row3"> + <div role="gridcell" id="treegrid_cell5">cell1</div> + <div role="gridcell" id="treegrid_cell6">cell2</div> + </div> + </div> + + <div id="headings"> + <h1 id="h1">heading1</h1> + <h2 id="h2">heading2</h2> + <h3 id="h3">heading3</h3> + <h4 id="h4">heading4</h4> + <h5 id="h5">heading5</h5> + <h6 id="h6">heading6</h6> + <div id="ariaHeadingNoLevel" role="heading">ariaHeadingNoLevel</div> + </div> + + <ul id="combo1" role="combobox">Password + <li id="combo1_opt1" role="option">Xyzzy</li> + <li id="combo1_opt2" role="option">Plughs</li> + <li id="combo1_opt3" role="option">Shazaam</li> + <li id="combo1_opt4" role="option">JoeSentMe</li> + </ul> + + <form> + <input type="radio" style="display: none;" name="group3"> + <input type="radio" id="radio5" name="group3"> + </form> + + <div role="table" aria-colcount="4" aria-rowcount="2" id="table"> + <div role="row" id="table_row" aria-rowindex="2"> + <div role="cell" id="table_cell" aria-colindex="3">cell</div> + </div> + </div> + + <div role="grid" aria-readonly="true"> + <div tabindex="-1"> + <div role="row" id="wrapped_row_1"> + <div role="gridcell">cell content</div> + </div> + </div> + <div tabindex="-1"> + <div role="row" id="wrapped_row_2"> + <div role="gridcell">cell content</div> + </div> + </div> + </div> + + <div role="list" aria-owns="t1_li1 t1_li2 t1_li3" id="aria-list_4"> + <div role="listitem" id="t1_li2">Apples</div> + <div role="listitem" id="t1_li1">Oranges</div> + </div> + <div role="listitem" id="t1_li3">Bananas</div> + + <!-- ARIA comments, 1 level, group pos and size calculation --> + <article> + <p id="comm_single_1" role="comment">Comment 1</p> + <p id="comm_single_2" role="comment">Comment 2</p> + </article> + + <!-- Nested comments --> + <article> + <div id="comm_nested_1" role="comment"><p>Comment 1 level 1</p> + <div id="comm_nested_1_1" role="comment"><p>Comment 1 level 2</p></div> + <div id="comm_nested_1_2" role="comment"><p>Comment 2 level 2</p></div> + </div> + <div id="comm_nested_2" role="comment"><p>Comment 2 level 1</p> + <div id="comm_nested_2_1" role="comment"><p>Comment 3 level 2</p> + <div id="comm_nested_2_1_1" role="comment"><p>Comment 1 level 3</p></div> + </div> + </div> + <div id="comm_nested_3" role="comment"><p>Comment 3 level 1</p></div> + </article> +</body> +</html> diff --git a/accessible/tests/mochitest/attributes/test_obj_group.xhtml b/accessible/tests/mochitest/attributes/test_obj_group.xhtml new file mode 100644 index 0000000000..0eda4b6f2d --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_obj_group.xhtml @@ -0,0 +1,215 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessibility Group Attributes ('level', 'setsize', 'posinset') Test."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../attributes.js" /> + + <script type="application/javascript"> + <![CDATA[ + function openMenu(aID) + { + this.menuNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, this.menuNode) + ]; + + this.invoke = function openMenu_invoke() + { + this.menuNode.open = true; + } + + this.finalCheck = function openMenu_finalCheck() + { + testGroupAttrs("menu_item1.1", 1, 1); + testGroupAttrs("menu_item1.2", 1, 3); + testGroupAttrs("menu_item1.4", 2, 3); + testGroupAttrs("menu_item2", 3, 3); + } + + this.getID = function openMenu_getID() + { + return "open menu " + prettyName(aID); + } + } + + function openSubMenu(aID) + { + this.menuNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, this.menuNode) + ]; + + this.invoke = function openSubMenu_invoke() + { + this.menuNode.open = true; + } + + this.finalCheck = function openSubMenu_finalCheck() + { + testGroupAttrs("menu_item2.1", 1, 2, 1); + testGroupAttrs("menu_item2.2", 2, 2, 1); + } + + this.getID = function openSubMenu_getID() + { + return "open submenu " + prettyName(aID); + } + } + + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // xul:listbox (bug 417317) + testGroupAttrs("listitem1", 1, 4); + testGroupAttrs("listitem2", 2, 4); + testGroupAttrs("listitem3", 3, 4); + testGroupAttrs("listitem4", 4, 4); + + ////////////////////////////////////////////////////////////////////////// + // xul:tab + testGroupAttrs("tab1", 1, 2); + testGroupAttrs("tab2", 2, 2); + + ////////////////////////////////////////////////////////////////////////// + // xul:radio + testGroupAttrs("radio1", 1, 2); + testGroupAttrs("radio2", 2, 2); + + ////////////////////////////////////////////////////////////////////////// + // xul:menulist + testGroupAttrs("menulist1.1", 1); + testGroupAttrs("menulist1.2", 2); + testGroupAttrs("menulist1.3", 3); + testGroupAttrs("menulist1.4", 4); + + ////////////////////////////////////////////////////////////////////////// + // ARIA menu (bug 441888) + testGroupAttrs("aria-menuitem", 1, 3); + testGroupAttrs("aria-menuitemcheckbox", 2, 3); + testGroupAttrs("aria-menuitemradio", 3, 3); + testGroupAttrs("aria-menuitem2", 1, 1); + + ////////////////////////////////////////////////////////////////////////// + // xul:menu (bug 443881) + gQueue = new eventQueue(); + gQueue.push(new openMenu("menu_item1")); + gQueue.push(new openSubMenu("menu_item2")); + gQueue.invoke(); // SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=417317" + title="Certain types of LISTITEM accessibles no longer get attributes set like 'x of y', regression from fix for bug 389926"> + Mozilla Bug 417317 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=443881" + title="take into account separators in xul menus when group attributes are calculating"> + Mozilla Bug 443881 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=441888" + title="ARIA checked menu items are not included in the total list of menu items"> + Mozilla Bug 441888 + </a><br/> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + + <richlistbox> + <richlistitem id="listitem1"/> + <richlistitem id="listitem2"><label value="listitem2"/></richlistitem> + <richlistitem id="listitem3"/> + <richlistitem id="listitem4"><label value="listitem4"/></richlistitem> + </richlistbox> + + <menubar> + <menu label="item1" id="menu_item1"> + <menupopup> + <menuitem label="item1.1" id="menu_item1.1"/> + <menuseparator/> + <menuitem label="item1.2" id="menu_item1.2"/> + <menuitem label="item1.3" hidden="true"/> + <menuitem label="item1.4" id="menu_item1.4"/> + <menu label="item2" id="menu_item2"> + <menupopup> + <menuitem label="item2.1" id="menu_item2.1"/> + <menuitem label="item2.2" id="menu_item2.2"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menubar> + + <tabbox> + <tabs> + <tab id="tab1" label="tab1"/> + <tab id="tab2" label="tab3"/> + </tabs> + <tabpanels> + <tabpanel/> + <tabpanel/> + </tabpanels> + </tabbox> + + <radiogroup> + <radio id="radio1" label="radio1"/> + <radio id="radio2" label="radio2"/> + </radiogroup> + + <menulist id="menulist1" label="Vehicle"> + <menupopup> + <menuitem id="menulist1.1" label="Car"/> + <menuitem id="menulist1.2" label="Taxi"/> + <menuitem id="menulist1.3" label="Bus" selected="true"/> + <menuitem id="menulist1.4" label="Train"/> + </menupopup> + </menulist> + + <vbox> + <description role="menuitem" id="aria-menuitem" + value="conventional menuitem"/> + <description role="menuitemcheckbox" id="aria-menuitemcheckbox" + value="conventional checkbox menuitem"/> + <description role="menuitem" hidden="true"/> + <description role="menuitemradio" id="aria-menuitemradio" + value="conventional radio menuitem"/> + <description role="separator" + value="conventional separator"/> + <description role="menuitem" id="aria-menuitem2" + value="conventional menuitem"/> + </vbox> + + </vbox> + </hbox> +</window> + diff --git a/accessible/tests/mochitest/attributes/test_obj_group_tree.xhtml b/accessible/tests/mochitest/attributes/test_obj_group_tree.xhtml new file mode 100644 index 0000000000..287ee7989e --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_obj_group_tree.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tree attributes tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../attributes.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + var treeNode = getNode("tree"); + + var tree = getAccessible(treeNode); + var treeitem1 = tree.firstChild.nextSibling; + testGroupAttrs(treeitem1, 1, 4, 1); + + var treeitem2 = treeitem1.nextSibling; + testGroupAttrs(treeitem2, 2, 4, 1); + + var treeitem3 = treeitem2.nextSibling; + testGroupAttrs(treeitem3, 1, 2, 2); + + var treeitem4 = treeitem3.nextSibling; + testGroupAttrs(treeitem4, 2, 2, 2); + + var treeitem5 = treeitem4.nextSibling; + testGroupAttrs(treeitem5, 3, 4, 1); + + var treeitem6 = treeitem5.nextSibling; + testGroupAttrs(treeitem6, 4, 4, 1); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "tree", new nsTreeTreeView()); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Mozilla Bug 503727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/attributes/test_tag.html b/accessible/tests/mochitest/attributes/test_tag.html new file mode 100644 index 0000000000..b57cc2dca8 --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_tag.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML landmark tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + function doTest() { + // And some AT may look for this + testAttrs("nav", {"tag": "nav"}, true); + testAttrs("header", {"tag": "header"}, true); + testAttrs("footer", {"tag": "footer"}, true); + testAttrs("article", {"tag": "article"}, true); + testAttrs("aside", {"tag": "aside"}, true); + testAttrs("section", {"tag": "section"}, true); + testAttrs("main", {"tag": "article"}, true); + testAttrs("form", {"tag": "article"}, true); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Provide mappings for html5 <nav> <header> <footer> <article>" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=593368"> + Bug 593368 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=613502" + title="Map <article> like we do aria role article"> + Bug 613502 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=610650" + title="Change implementation of HTML5 landmark elements to conform"> + Bug 610650 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=614310" + title="Map section to pane (like role=region)"> + Mozilla Bug 614310 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=734982" + title="Map ARIA role FORM"> + Bug 734982 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <nav id="nav">a nav</nav> + <header id="header">a header</header> + <footer id="footer">a footer</footer> + <aside id="aside">by the way I am an aside</aside> + <section id="section">a section</section> + + <article id="article">an article</article> + <article id="main" role="main">a main area</article> + <article id="form" role="form">a form area</article> + +</body> +</html> diff --git a/accessible/tests/mochitest/attributes/test_xml-roles.html b/accessible/tests/mochitest/attributes/test_xml-roles.html new file mode 100644 index 0000000000..b6cb95eb39 --- /dev/null +++ b/accessible/tests/mochitest/attributes/test_xml-roles.html @@ -0,0 +1,267 @@ +<!DOCTYPE html> +<html> +<head> + <title>XML roles tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + function doTest() { + // Some AT may look for this + testAttrs("nav", {"xml-roles": "navigation"}, true); + testAttrs("header", {"xml-roles": "banner"}, true); + testAbsentAttrs("article_header", {"xml-roles": "banner"}); + testAbsentAttrs("main_header", {"xml-roles": "banner"}); + testAbsentAttrs("section_header", {"xml-roles": "banner"}); + testAttrs("footer", {"xml-roles": "contentinfo"}, true); + testAbsentAttrs("article_footer", {"xml-roles": "contentinfo"}); + testAbsentAttrs("main_footer", {"xml-roles": "contentinfo"}); + testAbsentAttrs("section_footer", {"xml-roles": "contentinfo"}); + testAttrs("aside", {"xml-roles": "complementary"}, true); + testAbsentAttrs("section", {"xml-roles": "region"}, true); + testAttrs("main", {"xml-roles": "main"}, true); // // ARIA override + testAttrs("form", {"xml-roles": "form"}, true); + testAttrs("feed", {"xml-roles": "feed"}, true); + testAttrs("article", {"xml-roles": "article"}, true); + testAttrs("main_element", {"xml-roles": "main"}, true); + testAttrs("figure", {"xml-roles": "figure"}, true); + + testAttrs("search", {"xml-roles": "searchbox"}, true); + + testAttrs("code", {"xml-roles": "code"}, true); + + testAttrs("open-1", {"xml-roles": "open-fence"}, true); + testAttrs("open-2", {"xml-roles": "open-fence"}, true); + testAttrs("open-3", {"xml-roles": "open-fence"}, true); + testAttrs("open-4", {"xml-roles": "open-fence"}, true); + testAttrs("open-5", {"xml-roles": "open-fence"}, true); + testAttrs("open-6", {"xml-roles": "open-fence"}, true); + testAttrs("open-7", {"xml-roles": "open-fence"}, true); + + testAttrs("sep-1", {"xml-roles": "separator"}, true); + testAttrs("sep-2", {"xml-roles": "separator"}, true); + testAttrs("sep-3", {"xml-roles": "separator"}, true); + testAttrs("sep-4", {"xml-roles": "separator"}, true); + testAttrs("sep-5", {"xml-roles": "separator"}, true); + testAttrs("sep-6", {"xml-roles": "separator"}, true); + testAttrs("sep-7", {"xml-roles": "separator"}, true); + + testAttrs("close-1", {"xml-roles": "close-fence"}, true); + testAttrs("close-2", {"xml-roles": "close-fence"}, true); + testAttrs("close-3", {"xml-roles": "close-fence"}, true); + testAttrs("close-4", {"xml-roles": "close-fence"}, true); + testAttrs("close-5", {"xml-roles": "close-fence"}, true); + testAttrs("close-6", {"xml-roles": "close-fence"}, true); + testAttrs("close-7", {"xml-roles": "close-fence"}, true); + + testAttrs("num", {"xml-roles": "numerator"}, true); + testAttrs("den", {"xml-roles": "denominator"}, true); + + testAttrs("sub-1", {"xml-roles": "subscript"}, true); + testAttrs("sub-2", {"xml-roles": "subscript"}, true); + testAttrs("sub-3", {"xml-roles": "subscript"}, true); + testAttrs("sup-1", {"xml-roles": "superscript"}, true); + testAttrs("sup-2", {"xml-roles": "superscript"}, true); + testAttrs("sup-3", {"xml-roles": "superscript"}, true); + testAttrs("sup-4", {"xml-roles": "superscript"}, true); + testAttrs("presub-1", {"xml-roles": "presubscript"}, true); + testAttrs("presub-2", {"xml-roles": "presubscript"}, true); + testAttrs("presup-1", {"xml-roles": "presuperscript"}, true); + + testAttrs("under-1", {"xml-roles": "underscript"}, true); + testAttrs("under-2", {"xml-roles": "underscript"}, true); + testAttrs("over-1", {"xml-roles": "overscript"}, true); + testAttrs("over-2", {"xml-roles": "overscript"}, true); + + testAttrs("root-index-1", {"xml-roles": "root-index"}, true); + + testAttrs("base-1", {"xml-roles": "base"}, true); + testAttrs("base-2", {"xml-roles": "base"}, true); + testAttrs("base-3", {"xml-roles": "base"}, true); + testAttrs("base-4", {"xml-roles": "base"}, true); + testAttrs("base-5", {"xml-roles": "base"}, true); + testAttrs("base-6", {"xml-roles": "base"}, true); + testAttrs("base-7", {"xml-roles": "base"}, true); + testAttrs("base-8", {"xml-roles": "base"}, true); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Provide mappings for html5 <nav> <header> <footer> <article>" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=593368"> + Bug 593368 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=613502" + title="Map <article> like we do aria role article"> + Bug 613502 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=610650" + title="Change implementation of HTML5 landmark elements to conform"> + Bug 610650 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=614310" + title="Map section to pane (like role=region)"> + Mozilla Bug 614310 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=734982" + title="Map ARIA role FORM"> + Bug 734982 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=761891" + title="HTML5 article element should expose xml-roles:article object attribute"> + Bug 761891 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=849624" + title="modify HTML5 header and footer accessibility API mapping"> + Bug 849624 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1121518" + title="ARIA 1.1: Support role 'searchbox'"> + Bug 1121518 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1356049" + title="Map ARIA figure role"> + Bug 1356049 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <nav id="nav">a nav</nav> + <header id="header">a header</header> + <footer id="footer">a footer</footer> + <article id="article_with_header_and_footer"> + <header id="article_header">a header within an article</header> + <footer id="article_footer">a footer within an article</footer> + </article> + <main id="main_with_header_and_footer"> + <header id="main_header">a header within a main</header> + <footer id="main_footer">a footer within a main</footer> + </main> + <section id="section_with_header_and_footer"> + <header id="section_header">a header within an section</header> + <footer id="section_footer">a footer within an section</footer> + </section> + <aside id="aside">by the way I am an aside</aside> + <section id="section">a section</section> + <article id="main" role="main">a main area</article> + <article id="form" role="form">a form area</article> + <div id="feed" role="feed">a feed</div> + <article id="article">article</article> + <main id="main_element">another main area</main> + <div id="figure" role="figure">a figure</div> + + <input id="search" type="search"/> + + <div id="code" role="code"></div> + + <!-- open-fence, separator, close-fence --> + <math><mo id="open-1">(</mo><mi>x</mi><mo id="sep-1">,</mo><mi>y</mi><mo id="close-1">)</mo></math> + <math><mrow><mo id="open-2">(</mo><mi>x</mi><mo id="sep-2">,</mo><mi>y</mi><mo id="close-2">)</mo></mrow></math> + <math><mstyle><mo id="open-3">(</mo><mi>x</mi><mo id="sep-3">,</mo><mi>y</mi><mo id="close-3">)</mo></mstyle></math> + <math><msqrt><mo id="open-4">(</mo><mi>x</mi><mo id="sep-4">,</mo><mi>y</mi><mo id="close-4">)</mo></msqrt></math> + <math><menclose><mo id="open-5">(</mo><mi>x</mi><mo id="sep-5">,</mo><mi>y</mi><mo id="close-5">)</mo></menclose></math> + <math><merror><mo id="open-6">(</mo><mi>x</mi><mo id="sep-6">,</mo><mi>y</mi><mo id="close-6">)</mo></merror></math> + <math><mtable><mtr><mtd><mo id="open-7">(</mo><mi>x</mi><mo id="sep-7">,</mo><mi>y</mi><mo id="close-7">)</mo></mtd></mtr></mtable></math> + + <!-- numerator, denominator --> + <math> + <mfrac> + <mi id="num">a</mi> + <mi id="den">b</mi> + </mfrac> + </math> + + <!-- subscript, superscript, presubscript, presuperscript --> + <math> + <msub> + <mi id="base-1">a</mi> + <mi id="sub-1">b</mi> + </msub> + </math> + <math> + <msup> + <mi id="base-2">a</mi> + <mi id="sup-1">b</mi> + </msup> + </math> + <math> + <msubsup> + <mi id="base-3">a</mi> + <mi id="sub-2">b</mi> + <mi id="sup-2">c</mi> + </msubsup> + </math> + <math> + <mmultiscripts> + <mi id="base-4">a</mi> + <mi id="sub-3">b</mi> + <mi id="sup-3">c</mi> + <none/> + <mi id="sup-4">d</mi> + <mprescripts/> + <mi id="presub-1">e</mi> + <none/> + <mi id="presub-2">f</mi> + <mi id="presup-1">g</mi> + </mmultiscripts> + </math> + + <!-- underscript, overscript --> + <math> + <munder> + <mi id="base-5">a</mi> + <mi id="under-1">b</mi> + </munder> + </math> + <math> + <mover> + <mi id="base-6">a</mi> + <mi id="over-1">b</mi> + </mover> + </math> + <math> + <munderover> + <mi id="base-7">a</mi> + <mi id="under-2">b</mi> + <mi id="over-2">c</mi> + </munderover> + </math> + + <!-- root-index --> + <math> + <mroot> + <mi id="base-8">a</mi> + <mi id="root-index-1">b</mi> + </mroot> + </math> + +</body> +</html> diff --git a/accessible/tests/mochitest/autocomplete.js b/accessible/tests/mochitest/autocomplete.js new file mode 100644 index 0000000000..130d9526e7 --- /dev/null +++ b/accessible/tests/mochitest/autocomplete.js @@ -0,0 +1,195 @@ +const nsISupports = Ci.nsISupports; +const nsIAutoCompleteResult = Ci.nsIAutoCompleteResult; +const nsIAutoCompleteSearch = Ci.nsIAutoCompleteSearch; +const nsIFactory = Ci.nsIFactory; +const nsIUUIDGenerator = Ci.nsIUUIDGenerator; +const nsIComponentRegistrar = Ci.nsIComponentRegistrar; + +var gDefaultAutoCompleteSearch = null; + +/** + * Register 'test-a11y-search' AutoCompleteSearch. + * + * @param aValues [in] set of possible results values + * @param aComments [in] set of possible results descriptions + */ +function initAutoComplete(aValues, aComments) { + var allResults = new ResultsHeap(aValues, aComments); + gDefaultAutoCompleteSearch = new AutoCompleteSearch( + "test-a11y-search", + allResults + ); + registerAutoCompleteSearch( + gDefaultAutoCompleteSearch, + "Accessibility Test AutoCompleteSearch" + ); +} + +/** + * Unregister 'test-a11y-search' AutoCompleteSearch. + */ +function shutdownAutoComplete() { + unregisterAutoCompleteSearch(gDefaultAutoCompleteSearch); + gDefaultAutoCompleteSearch.cid = null; + gDefaultAutoCompleteSearch = null; +} + +/** + * Register the given AutoCompleteSearch. + * + * @param aSearch [in] AutoCompleteSearch object + * @param aDescription [in] description of the search object + */ +function registerAutoCompleteSearch(aSearch, aDescription) { + var name = "@mozilla.org/autocomplete/search;1?name=" + aSearch.name; + + var uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService( + nsIUUIDGenerator + ); + var cid = uuidGenerator.generateUUID(); + + var componentManager = Components.manager.QueryInterface( + nsIComponentRegistrar + ); + componentManager.registerFactory(cid, aDescription, name, aSearch); + + // Keep the id on the object so we can unregister later. + aSearch.cid = cid; +} + +/** + * Unregister the given AutoCompleteSearch. + */ +function unregisterAutoCompleteSearch(aSearch) { + var componentManager = Components.manager.QueryInterface( + nsIComponentRegistrar + ); + componentManager.unregisterFactory(aSearch.cid, aSearch); +} + +/** + * A container to keep all possible results of autocomplete search. + */ +function ResultsHeap(aValues, aComments) { + this.values = aValues; + this.comments = aComments; +} + +ResultsHeap.prototype = { + constructor: ResultsHeap, + + /** + * Return AutoCompleteResult for the given search string. + */ + getAutoCompleteResultFor(aSearchString) { + var values = [], + comments = []; + for (var idx = 0; idx < this.values.length; idx++) { + if (this.values[idx].includes(aSearchString)) { + values.push(this.values[idx]); + comments.push(this.comments[idx]); + } + } + return new AutoCompleteResult(values, comments); + }, +}; + +/** + * nsIAutoCompleteSearch implementation. + * + * @param aName [in] the name of autocomplete search + * @param aAllResults [in] ResultsHeap object + */ +function AutoCompleteSearch(aName, aAllResults) { + this.name = aName; + this.allResults = aAllResults; +} + +AutoCompleteSearch.prototype = { + constructor: AutoCompleteSearch, + + // nsIAutoCompleteSearch implementation + startSearch(aSearchString, aSearchParam, aPreviousResult, aListener) { + var result = this.allResults.getAutoCompleteResultFor(aSearchString); + aListener.onSearchResult(this, result); + }, + + stopSearch() {}, + + // nsISupports implementation + QueryInterface: ChromeUtils.generateQI([ + "nsIFactory", + "nsIAutoCompleteSearch", + ]), + + // nsIFactory implementation + createInstance(outer, iid) { + return this.QueryInterface(iid); + }, + + // Search name. Used by AutoCompleteController. + name: null, + + // Results heap. + allResults: null, +}; + +/** + * nsIAutoCompleteResult implementation. + */ +function AutoCompleteResult(aValues, aComments) { + this.values = aValues; + this.comments = aComments; + + if (this.values.length > 0) { + this.searchResult = nsIAutoCompleteResult.RESULT_SUCCESS; + } else { + this.searchResult = nsIAutoCompleteResult.NOMATCH; + } +} + +AutoCompleteResult.prototype = { + constructor: AutoCompleteResult, + + searchString: "", + searchResult: null, + + defaultIndex: 0, + + get matchCount() { + return this.values.length; + }, + + getValueAt(aIndex) { + return this.values[aIndex]; + }, + + getLabelAt(aIndex) { + return this.getValueAt(aIndex); + }, + + getCommentAt(aIndex) { + return this.comments[aIndex]; + }, + + getStyleAt(aIndex) { + return null; + }, + + getImageAt(aIndex) { + return ""; + }, + + getFinalCompleteValueAt(aIndex) { + return this.getValueAt(aIndex); + }, + + removeValueAt(aRowIndex) {}, + + // nsISupports implementation + QueryInterface: ChromeUtils.generateQI(["nsIAutoCompleteResult"]), + + // Data + values: null, + comments: null, +}; diff --git a/accessible/tests/mochitest/bounds/a11y.ini b/accessible/tests/mochitest/bounds/a11y.ini new file mode 100644 index 0000000000..bdb8c02f90 --- /dev/null +++ b/accessible/tests/mochitest/bounds/a11y.ini @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_list.html] +[test_select.html] diff --git a/accessible/tests/mochitest/bounds/test_list.html b/accessible/tests/mochitest/bounds/test_list.html new file mode 100644 index 0000000000..7e5b75868d --- /dev/null +++ b/accessible/tests/mochitest/bounds/test_list.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html> +<head> + <title>Accessible boundaries when page is zoomed</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + + <script type="application/javascript"> + function doTest() { + // Inside list + var li = getAccessible("insidelist_item"); + testBounds(li); + + var [xLI, yLI, widthLI, heightLI] = getBounds(li); + var bullet = li.firstChild; + var [x, y, width, height] = getBounds(bullet); + is(x, xLI, + "Bullet x should match to list item x"); + ok(y >= yLI, + "Bullet y= " + y + " should be not less than list item y=" + yLI); + ok(width < widthLI, + "Bullet width should be lesser list item width"); + ok(height <= heightLI, + "Bullet height= " + height + " should be not greater than list item height=" + heightLI); + + // Outside list + li = getAccessible("outsidelist_item"); + var [xLIElm, yLIElm, widthLIElm, heightLIElm] = getBoundsForDOMElm(li); + [xLI, yLI, widthLI, heightLI] = getBounds(li); + + ok(xLI < xLIElm, + "Outside list item x=" + xLI + " should be lesser than list item element x=" + xLIElm); + is(yLI, yLIElm, + "Outside list item y should match to list item element y"); + ok(widthLI > widthLIElm, + "Outside list item width=" + widthLI + " should be greater than list item element width=" + widthLIElm); + ok(heightLI >= Math.trunc(heightLIElm), + "Outside list item height=" + heightLI + " should not be less than list item element height=" + heightLIElm); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=754627" + title="GetBounds on bullet return wrong values"> + Mozilla Bug 754627 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <ul style="list-style-position: inside;"> + <li id="insidelist_item">item</li> + </ul> + + <ul style="list-style-position: outside;"> + <li id="outsidelist_item">item</li> + </ul> + +</body> +</html> diff --git a/accessible/tests/mochitest/bounds/test_select.html b/accessible/tests/mochitest/bounds/test_select.html new file mode 100644 index 0000000000..ac9a501f32 --- /dev/null +++ b/accessible/tests/mochitest/bounds/test_select.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html> +<head> + <title>Accessible boundaries when page is zoomed</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function openComboboxNCheckBounds(aID) { + this.combobox = getAccessible(aID); + this.comboboxList = this.combobox.firstChild; + this.comboboxOption = this.comboboxList.firstChild; + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, this.comboboxOption), + ]; + + this.invoke = function openComboboxNCheckBounds_invoke() { + getNode(aID).focus(); + synthesizeKey("VK_DOWN", { altKey: true }); + }; + + this.finalCheck = function openComboboxNCheckBounds_invoke() { + testBounds(this.comboboxOption); + }; + + this.getID = function openComboboxNCheckBounds_getID() { + return "open combobox and test boundaries"; + }; + } + + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTest() { + // Combobox + testBounds("combobox"); + + // Option boundaries matches to combobox boundaries when collapsed. + var selectBounds = getBoundsForDOMElm("combobox"); + testBounds("option1", selectBounds); + + // Open combobox and test option boundaries. + gQueue = new eventQueue(); + gQueue.push(new openComboboxNCheckBounds("combobox")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="combobox"> + <option id="option1">item1</option> + <option>item2</option> + </select> +</body> +</html> diff --git a/accessible/tests/mochitest/browser.js b/accessible/tests/mochitest/browser.js new file mode 100644 index 0000000000..ffe67b0ed0 --- /dev/null +++ b/accessible/tests/mochitest/browser.js @@ -0,0 +1,157 @@ +/* import-globals-from common.js */ + +var { AppConstants } = ChromeUtils.import( + "resource://gre/modules/AppConstants.jsm" +); + +/** + * Load the browser with the given url and then invokes the given function. + */ +function openBrowserWindow(aFunc, aURL, aRect) { + gBrowserContext.testFunc = aFunc; + gBrowserContext.startURL = aURL; + gBrowserContext.browserRect = aRect; + + addLoadEvent(openBrowserWindowIntl); +} + +/** + * Close the browser window. + */ +function closeBrowserWindow() { + gBrowserContext.browserWnd.close(); +} + +/** + * Return the browser window object. + */ +function browserWindow() { + return gBrowserContext.browserWnd; +} + +/** + * Return the document of the browser window. + */ +function browserDocument() { + return browserWindow().document; +} + +/** + * Return tab browser object. + */ +function tabBrowser() { + return browserWindow().gBrowser; +} + +/** + * Return browser element of the current tab. + */ +function currentBrowser() { + return tabBrowser().selectedBrowser; +} + +/** + * Return DOM document of the current tab. + */ +function currentTabDocument() { + return currentBrowser().contentDocument; +} + +/** + * Return window of the current tab. + */ +function currentTabWindow() { + return currentTabDocument().defaultView; +} + +/** + * Return browser element of the tab at the given index. + */ +function browserAt(aIndex) { + return tabBrowser().getBrowserAtIndex(aIndex); +} + +/** + * Return DOM document of the tab at the given index. + */ +function tabDocumentAt(aIndex) { + return browserAt(aIndex).contentDocument; +} + +/** + * Return input element of address bar. + */ +function urlbarInput() { + return browserWindow().document.getElementById("urlbar").inputField; +} + +/** + * Return reload button. + */ +function reloadButton() { + return browserWindow().document.getElementById("urlbar-reload-button"); +} + +// ////////////////////////////////////////////////////////////////////////////// +// private section + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +var gBrowserContext = { + browserWnd: null, + testFunc: null, + startURL: "", +}; + +function openBrowserWindowIntl() { + var params = "chrome,all,dialog=no,non-remote"; + var rect = gBrowserContext.browserRect; + if (rect) { + if ("left" in rect) { + params += ",left=" + rect.left; + } + if ("top" in rect) { + params += ",top=" + rect.top; + } + if ("width" in rect) { + params += ",width=" + rect.width; + } + if ("height" in rect) { + params += ",height=" + rect.height; + } + } + + gBrowserContext.browserWnd = window.browsingContext.topChromeWindow.openDialog( + AppConstants.BROWSER_CHROME_URL, + "_blank", + params, + gBrowserContext.startURL || "data:text/html,<html></html>" + ); + + whenDelayedStartupFinished(browserWindow(), function() { + addA11yLoadEvent(startBrowserTests, browserWindow()); + }); +} + +function startBrowserTests() { + if (gBrowserContext.startURL) { + // Make sure the window is the one loading our URL. + if (currentBrowser().contentWindow.location != gBrowserContext.startURL) { + setTimeout(startBrowserTests, 0); + return; + } + // wait for load + addA11yLoadEvent(gBrowserContext.testFunc, currentBrowser().contentWindow); + } else { + gBrowserContext.testFunc(); + } +} + +function whenDelayedStartupFinished(aWindow, aCallback) { + Services.obs.addObserver(function observer(aSubject, aTopic) { + if (aWindow == aSubject) { + Services.obs.removeObserver(observer, aTopic); + setTimeout(aCallback, 0); + } + }, "browser-delayed-startup-finished"); +} diff --git a/accessible/tests/mochitest/common.js b/accessible/tests/mochitest/common.js new file mode 100644 index 0000000000..69014c6cd3 --- /dev/null +++ b/accessible/tests/mochitest/common.js @@ -0,0 +1,1076 @@ +// This file has circular dependencies that may require other files. Rather +// than use import-globals-from, we list the globals individually here to save +// confusing ESLint. +// actions.js +/* globals testActionNames */ +// attributes.js +/* globals testAttrs, testAbsentAttrs, testTextAttrs */ +// relations.js +/* globals testRelation */ +// role.js +/* globals isRole */ +// state.js +/* globals testStates */ + +// ////////////////////////////////////////////////////////////////////////////// +// Interfaces + +const nsIAccessibilityService = Ci.nsIAccessibilityService; + +const nsIAccessibleEvent = Ci.nsIAccessibleEvent; +const nsIAccessibleStateChangeEvent = Ci.nsIAccessibleStateChangeEvent; +const nsIAccessibleCaretMoveEvent = Ci.nsIAccessibleCaretMoveEvent; +const nsIAccessibleScrollingEvent = Ci.nsIAccessibleScrollingEvent; +const nsIAccessibleTextChangeEvent = Ci.nsIAccessibleTextChangeEvent; +const nsIAccessibleTextSelectionChangeEvent = + Ci.nsIAccessibleTextSelectionChangeEvent; +const nsIAccessibleVirtualCursorChangeEvent = + Ci.nsIAccessibleVirtualCursorChangeEvent; +const nsIAccessibleObjectAttributeChangedEvent = + Ci.nsIAccessibleObjectAttributeChangedEvent; +const nsIAccessibleAnnouncementEvent = Ci.nsIAccessibleAnnouncementEvent; + +const nsIAccessibleStates = Ci.nsIAccessibleStates; +const nsIAccessibleRole = Ci.nsIAccessibleRole; +const nsIAccessibleScrollType = Ci.nsIAccessibleScrollType; +const nsIAccessibleCoordinateType = Ci.nsIAccessibleCoordinateType; + +const nsIAccessibleRelation = Ci.nsIAccessibleRelation; +const nsIAccessibleTextRange = Ci.nsIAccessibleTextRange; + +const nsIAccessible = Ci.nsIAccessible; + +const nsIAccessibleDocument = Ci.nsIAccessibleDocument; +const nsIAccessibleApplication = Ci.nsIAccessibleApplication; + +const nsIAccessibleText = Ci.nsIAccessibleText; +const nsIAccessibleEditableText = Ci.nsIAccessibleEditableText; + +const nsIAccessibleHyperLink = Ci.nsIAccessibleHyperLink; +const nsIAccessibleHyperText = Ci.nsIAccessibleHyperText; + +const nsIAccessibleImage = Ci.nsIAccessibleImage; +const nsIAccessiblePivot = Ci.nsIAccessiblePivot; +const nsIAccessibleSelectable = Ci.nsIAccessibleSelectable; +const nsIAccessibleTable = Ci.nsIAccessibleTable; +const nsIAccessibleTableCell = Ci.nsIAccessibleTableCell; +const nsIAccessibleTraversalRule = Ci.nsIAccessibleTraversalRule; +const nsIAccessibleValue = Ci.nsIAccessibleValue; + +const nsIObserverService = Ci.nsIObserverService; + +const nsIDOMWindow = Ci.nsIDOMWindow; + +const nsIPropertyElement = Ci.nsIPropertyElement; + +// ////////////////////////////////////////////////////////////////////////////// +// OS detect + +const MAC = navigator.platform.includes("Mac"); +const LINUX = navigator.platform.includes("Linux"); +const SOLARIS = navigator.platform.includes("SunOS"); +const WIN = navigator.platform.includes("Win"); + +// ////////////////////////////////////////////////////////////////////////////// +// Application detect +// Firefox is assumed by default. + +const SEAMONKEY = navigator.userAgent.match(/ SeaMonkey\//); + +// ////////////////////////////////////////////////////////////////////////////// +// Accessible general + +const STATE_BUSY = nsIAccessibleStates.STATE_BUSY; + +const SCROLL_TYPE_ANYWHERE = nsIAccessibleScrollType.SCROLL_TYPE_ANYWHERE; + +const COORDTYPE_SCREEN_RELATIVE = + nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE; +const COORDTYPE_WINDOW_RELATIVE = + nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE; +const COORDTYPE_PARENT_RELATIVE = + nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE; + +const kEmbedChar = String.fromCharCode(0xfffc); + +const kDiscBulletChar = String.fromCharCode(0x2022); +const kDiscBulletText = kDiscBulletChar + " "; +const kCircleBulletText = String.fromCharCode(0x25e6) + " "; +const kSquareBulletText = String.fromCharCode(0x25fe) + " "; + +const MAX_TRIM_LENGTH = 100; + +/** + * Services to determine if e10s is enabled. + */ +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +/** + * nsIAccessibilityService service. + */ +var gAccService = Cc["@mozilla.org/accessibilityService;1"].getService( + nsIAccessibilityService +); + +/** + * Enable/disable logging. + */ +function enableLogging(aModules) { + gAccService.setLogging(aModules); +} +function disableLogging() { + gAccService.setLogging(""); +} +function isLogged(aModule) { + return gAccService.isLogged(aModule); +} + +/** + * Dumps the accessible tree into console. + */ +function dumpTree(aId, aMsg) { + function dumpTreeIntl(acc, indent) { + dump(indent + prettyName(acc) + "\n"); + + var children = acc.children; + for (var i = 0; i < children.length; i++) { + var child = children.queryElementAt(i, nsIAccessible); + dumpTreeIntl(child, indent + " "); + } + } + + function dumpDOMTreeIntl(node, indent) { + dump(indent + prettyName(node) + "\n"); + + var children = node.childNodes; + for (var i = 0; i < children.length; i++) { + var child = children.item(i); + dumpDOMTreeIntl(child, indent + " "); + } + } + + dump(aMsg + "\n"); + var root = getAccessible(aId); + dumpTreeIntl(root, " "); + + dump("DOM tree:\n"); + dumpDOMTreeIntl(getNode(aId), " "); +} + +/** + * Invokes the given function when document is loaded and focused. Preferable + * to mochitests 'addLoadEvent' function -- additionally ensures state of the + * document accessible is not busy. + * + * @param aFunc the function to invoke + */ +function addA11yLoadEvent(aFunc, aWindow) { + function waitForDocLoad() { + window.setTimeout(function() { + var targetDocument = aWindow ? aWindow.document : document; + var accDoc = getAccessible(targetDocument); + var state = {}; + accDoc.getState(state, {}); + if (state.value & STATE_BUSY) { + waitForDocLoad(); + return; + } + + window.setTimeout(aFunc, 0); + }, 0); + } + + if ( + aWindow && + aWindow.document.activeElement && + aWindow.document.activeElement.localName == "browser" + ) { + waitForDocLoad(); + } else { + SimpleTest.waitForFocus(waitForDocLoad, aWindow); + } +} + +/** + * Analogy of SimpleTest.is function used to compare objects. + */ +function isObject(aObj, aExpectedObj, aMsg) { + if (aObj == aExpectedObj) { + ok(true, aMsg); + return; + } + + ok( + false, + aMsg + + " - got '" + + prettyName(aObj) + + "', expected '" + + prettyName(aExpectedObj) + + "'" + ); +} + +/** + * is() function checking the expected value is within the range. + */ +function isWithin(aExpected, aGot, aWithin, aMsg) { + if (Math.abs(aGot - aExpected) <= aWithin) { + ok(true, `${aMsg} - Got ${aGot}`); + } else { + ok( + false, + `${aMsg} - Got ${aGot}, expected ${aExpected} with error of ${aWithin}` + ); + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// Helpers for getting DOM node/accessible + +/** + * Return the DOM node by identifier (may be accessible, DOM node or ID). + */ +function getNode(aAccOrNodeOrID, aDocument) { + if (!aAccOrNodeOrID) { + return null; + } + + if (Node.isInstance(aAccOrNodeOrID)) { + return aAccOrNodeOrID; + } + + if (aAccOrNodeOrID instanceof nsIAccessible) { + return aAccOrNodeOrID.DOMNode; + } + + var node = (aDocument || document).getElementById(aAccOrNodeOrID); + if (!node) { + ok(false, "Can't get DOM element for " + aAccOrNodeOrID); + return null; + } + + return node; +} + +/** + * Constants indicates getAccessible doesn't fail if there is no accessible. + */ +const DONOTFAIL_IF_NO_ACC = 1; + +/** + * Constants indicates getAccessible won't fail if accessible doesn't implement + * the requested interfaces. + */ +const DONOTFAIL_IF_NO_INTERFACE = 2; + +/** + * Return accessible for the given identifier (may be ID attribute or DOM + * element or accessible object) or null. + * + * @param aAccOrElmOrID [in] identifier to get an accessible implementing + * the given interfaces + * @param aInterfaces [in, optional] the interface or an array interfaces + * to query it/them from obtained accessible + * @param aElmObj [out, optional] object to store DOM element which + * accessible is obtained for + * @param aDoNotFailIf [in, optional] no error for special cases (see + * constants above) + */ +function getAccessible(aAccOrElmOrID, aInterfaces, aElmObj, aDoNotFailIf) { + if (!aAccOrElmOrID) { + return null; + } + + var elm = null; + + if (aAccOrElmOrID instanceof nsIAccessible) { + try { + elm = aAccOrElmOrID.DOMNode; + } catch (e) {} + } else if (Node.isInstance(aAccOrElmOrID)) { + elm = aAccOrElmOrID; + } else { + elm = document.getElementById(aAccOrElmOrID); + if (!elm) { + ok(false, "Can't get DOM element for " + aAccOrElmOrID); + return null; + } + } + + if (aElmObj && typeof aElmObj == "object") { + aElmObj.value = elm; + } + + var acc = aAccOrElmOrID instanceof nsIAccessible ? aAccOrElmOrID : null; + if (!acc) { + try { + acc = gAccService.getAccessibleFor(elm); + } catch (e) {} + + if (!acc) { + if (!(aDoNotFailIf & DONOTFAIL_IF_NO_ACC)) { + ok(false, "Can't get accessible for " + prettyName(aAccOrElmOrID)); + } + + return null; + } + } + + if (!aInterfaces) { + return acc; + } + + if (!(aInterfaces instanceof Array)) { + aInterfaces = [aInterfaces]; + } + + for (var index = 0; index < aInterfaces.length; index++) { + if (acc instanceof aInterfaces[index]) { + continue; + } + try { + acc.QueryInterface(aInterfaces[index]); + } catch (e) { + if (!(aDoNotFailIf & DONOTFAIL_IF_NO_INTERFACE)) { + ok( + false, + "Can't query " + aInterfaces[index] + " for " + aAccOrElmOrID + ); + } + + return null; + } + } + + return acc; +} + +/** + * Return true if the given identifier has an accessible, or exposes the wanted + * interfaces. + */ +function isAccessible(aAccOrElmOrID, aInterfaces) { + return !!getAccessible( + aAccOrElmOrID, + aInterfaces, + null, + DONOTFAIL_IF_NO_ACC | DONOTFAIL_IF_NO_INTERFACE + ); +} + +/** + * Return an accessible that contains the DOM node for the given identifier. + */ +function getContainerAccessible(aAccOrElmOrID) { + var node = getNode(aAccOrElmOrID); + if (!node) { + return null; + } + + // eslint-disable-next-line no-empty + while ((node = node.parentNode) && !isAccessible(node)) {} + return node ? getAccessible(node) : null; +} + +/** + * Return root accessible for the given identifier. + */ +function getRootAccessible(aAccOrElmOrID) { + var acc = getAccessible(aAccOrElmOrID ? aAccOrElmOrID : document); + return acc ? acc.rootDocument.QueryInterface(nsIAccessible) : null; +} + +/** + * Return tab document accessible the given accessible is contained by. + */ +function getTabDocAccessible(aAccOrElmOrID) { + var acc = getAccessible(aAccOrElmOrID ? aAccOrElmOrID : document); + + var docAcc = acc.document.QueryInterface(nsIAccessible); + var containerDocAcc = docAcc.parent.document; + + // Test is running is stand-alone mode. + if (acc.rootDocument == containerDocAcc) { + return docAcc; + } + + // In the case of running all tests together. + return containerDocAcc.QueryInterface(nsIAccessible); +} + +/** + * Return application accessible. + */ +function getApplicationAccessible() { + return gAccService + .getApplicationAccessible() + .QueryInterface(nsIAccessibleApplication); +} + +/** + * A version of accessible tree testing, doesn't fail if tree is not complete. + */ +function testElm(aID, aTreeObj) { + testAccessibleTree(aID, aTreeObj, kSkipTreeFullCheck); +} + +/** + * Flags used for testAccessibleTree + */ +const kSkipTreeFullCheck = 1; + +/** + * Compare expected and actual accessibles trees. + * + * @param aAccOrElmOrID [in] accessible identifier + * @param aAccTree [in] JS object, each field corresponds to property of + * accessible object. Additionally special properties + * are presented: + * children - an array of JS objects representing + * children of accessible + * states - an object having states and extraStates + * fields + * @param aFlags [in, optional] flags, see constants above + */ +// eslint-disable-next-line complexity +function testAccessibleTree(aAccOrElmOrID, aAccTree, aFlags) { + var acc = getAccessible(aAccOrElmOrID); + if (!acc) { + return; + } + + var accTree = aAccTree; + + // Support of simplified accessible tree object. + accTree = normalizeAccTreeObj(accTree); + + // Test accessible properties. + for (var prop in accTree) { + var msg = + "Wrong value of property '" + prop + "' for " + prettyName(acc) + "."; + + switch (prop) { + case "actions": { + testActionNames(acc, accTree.actions); + break; + } + + case "attributes": + testAttrs(acc, accTree[prop], true); + break; + + case "absentAttributes": + testAbsentAttrs(acc, accTree[prop]); + break; + + case "interfaces": { + var ifaces = + accTree[prop] instanceof Array ? accTree[prop] : [accTree[prop]]; + for (let i = 0; i < ifaces.length; i++) { + ok( + acc instanceof ifaces[i], + "No " + ifaces[i] + " interface on " + prettyName(acc) + ); + } + break; + } + + case "relations": { + for (var rel in accTree[prop]) { + testRelation(acc, window[rel], accTree[prop][rel]); + } + break; + } + + case "role": + isRole(acc, accTree[prop], msg); + break; + + case "states": + case "extraStates": + case "absentStates": + case "absentExtraStates": { + testStates( + acc, + accTree.states, + accTree.extraStates, + accTree.absentStates, + accTree.absentExtraStates + ); + break; + } + + case "tagName": + is(accTree[prop], acc.DOMNode.tagName, msg); + break; + + case "textAttrs": { + var prevOffset = -1; + for (var offset in accTree[prop]) { + if (prevOffset != -1) { + let attrs = accTree[prop][prevOffset]; + testTextAttrs( + acc, + prevOffset, + attrs, + {}, + prevOffset, + +offset, + true + ); + } + prevOffset = +offset; + } + + if (prevOffset != -1) { + var charCount = getAccessible(acc, [nsIAccessibleText]) + .characterCount; + let attrs = accTree[prop][prevOffset]; + testTextAttrs( + acc, + prevOffset, + attrs, + {}, + prevOffset, + charCount, + true + ); + } + + break; + } + + default: + if (prop.indexOf("todo_") == 0) { + todo(false, msg); + } else if (prop != "children") { + is(acc[prop], accTree[prop], msg); + } + } + } + + // Test children. + if ("children" in accTree && accTree.children instanceof Array) { + var children = acc.children; + var childCount = children.length; + + if (accTree.children.length != childCount) { + for (let i = 0; i < Math.max(accTree.children.length, childCount); i++) { + var accChild = null, + testChild = null; + try { + testChild = accTree.children[i]; + accChild = children.queryElementAt(i, nsIAccessible); + + if (!testChild) { + ok( + false, + prettyName(acc) + + " has an extra child at index " + + i + + " : " + + prettyName(accChild) + ); + continue; + } + + testChild = normalizeAccTreeObj(testChild); + if (accChild.role !== testChild.role) { + ok( + false, + prettyName(accTree) + + " and " + + prettyName(acc) + + " have different children at index " + + i + + " : " + + prettyName(testChild) + + ", " + + prettyName(accChild) + ); + } + info( + "Matching " + + prettyName(accTree) + + " and " + + prettyName(acc) + + " child at index " + + i + + " : " + + prettyName(accChild) + ); + } catch (e) { + ok( + false, + prettyName(accTree) + + " is expected to have a child at index " + + i + + " : " + + prettyName(testChild) + + ", original tested: " + + prettyName(aAccOrElmOrID) + + ", " + + e + ); + } + } + } else { + if (aFlags & kSkipTreeFullCheck) { + for (let i = 0; i < childCount; i++) { + let child = children.queryElementAt(i, nsIAccessible); + testAccessibleTree(child, accTree.children[i], aFlags); + } + return; + } + + // nsIAccessible::firstChild + var expectedFirstChild = + childCount > 0 ? children.queryElementAt(0, nsIAccessible) : null; + var firstChild = null; + try { + firstChild = acc.firstChild; + } catch (e) {} + is( + firstChild, + expectedFirstChild, + "Wrong first child of " + prettyName(acc) + ); + + // nsIAccessible::lastChild + var expectedLastChild = + childCount > 0 + ? children.queryElementAt(childCount - 1, nsIAccessible) + : null; + var lastChild = null; + try { + lastChild = acc.lastChild; + } catch (e) {} + is( + lastChild, + expectedLastChild, + "Wrong last child of " + prettyName(acc) + ); + + for (var i = 0; i < childCount; i++) { + let child = children.queryElementAt(i, nsIAccessible); + + // nsIAccessible::parent + var parent = null; + try { + parent = child.parent; + } catch (e) {} + is(parent, acc, "Wrong parent of " + prettyName(child)); + + // nsIAccessible::indexInParent + var indexInParent = -1; + try { + indexInParent = child.indexInParent; + } catch (e) {} + is(indexInParent, i, "Wrong index in parent of " + prettyName(child)); + + // nsIAccessible::nextSibling + var expectedNextSibling = + i < childCount - 1 + ? children.queryElementAt(i + 1, nsIAccessible) + : null; + var nextSibling = null; + try { + nextSibling = child.nextSibling; + } catch (e) {} + is( + nextSibling, + expectedNextSibling, + "Wrong next sibling of " + prettyName(child) + ); + + // nsIAccessible::previousSibling + var expectedPrevSibling = + i > 0 ? children.queryElementAt(i - 1, nsIAccessible) : null; + var prevSibling = null; + try { + prevSibling = child.previousSibling; + } catch (e) {} + is( + prevSibling, + expectedPrevSibling, + "Wrong previous sibling of " + prettyName(child) + ); + + // Go down through subtree + testAccessibleTree(child, accTree.children[i], aFlags); + } + } + } +} + +/** + * Return true if accessible for the given node is in cache. + */ +function isAccessibleInCache(aNodeOrId) { + var node = getNode(aNodeOrId); + return !!gAccService.getAccessibleFromCache(node); +} + +/** + * Test accessible tree for defunct accessible. + * + * @param aAcc [in] the defunct accessible + * @param aNodeOrId [in] the DOM node identifier for the defunct accessible + */ +function testDefunctAccessible(aAcc, aNodeOrId) { + if (aNodeOrId) { + ok( + !isAccessible(aNodeOrId), + "Accessible for " + aNodeOrId + " wasn't properly shut down!" + ); + } + + var msg = + " doesn't fail for shut down accessible " + prettyName(aNodeOrId) + "!"; + + // firstChild + var success = false; + try { + aAcc.firstChild; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "firstChild" + msg); + + // lastChild + success = false; + try { + aAcc.lastChild; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "lastChild" + msg); + + // childCount + success = false; + try { + aAcc.childCount; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "childCount" + msg); + + // children + success = false; + try { + aAcc.children; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "children" + msg); + + // nextSibling + success = false; + try { + aAcc.nextSibling; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "nextSibling" + msg); + + // previousSibling + success = false; + try { + aAcc.previousSibling; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "previousSibling" + msg); + + // parent + success = false; + try { + aAcc.parent; + } catch (e) { + success = e.result == Cr.NS_ERROR_FAILURE; + } + ok(success, "parent" + msg); +} + +/** + * Convert role to human readable string. + */ +function roleToString(aRole) { + return gAccService.getStringRole(aRole); +} + +/** + * Convert states to human readable string. + */ +function statesToString(aStates, aExtraStates) { + var list = gAccService.getStringStates(aStates, aExtraStates); + + var str = ""; + for (var index = 0; index < list.length - 1; index++) { + str += list.item(index) + ", "; + } + + if (list.length != 0) { + str += list.item(index); + } + + return str; +} + +/** + * Convert event type to human readable string. + */ +function eventTypeToString(aEventType) { + return gAccService.getStringEventType(aEventType); +} + +/** + * Convert relation type to human readable string. + */ +function relationTypeToString(aRelationType) { + return gAccService.getStringRelationType(aRelationType); +} + +function getLoadContext() { + return window.docShell.QueryInterface(Ci.nsILoadContext); +} + +/** + * Return text from clipboard. + */ +function getTextFromClipboard() { + var trans = Cc["@mozilla.org/widget/transferable;1"].createInstance( + Ci.nsITransferable + ); + trans.init(getLoadContext()); + if (!trans) { + return ""; + } + + trans.addDataFlavor("text/unicode"); + Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard); + + var str = {}; + trans.getTransferData("text/unicode", str); + + if (str) { + str = str.value.QueryInterface(Ci.nsISupportsString); + } + if (str) { + return str.data; + } + + return ""; +} + +/** + * Extract DOMNode id from an accessible. If e10s is enabled, DOMNode is not + * present in parent process but, if available, DOMNode id is attached to an + * accessible object. + * @param {nsIAccessible} accessible accessible + * @return {String?} DOMNode id if available + */ +function getAccessibleDOMNodeID(accessible) { + if (accessible instanceof 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; +} + +/** + * Return pretty name for identifier, it may be ID, DOM node or accessible. + */ +function prettyName(aIdentifier) { + if (aIdentifier instanceof Array) { + let msg = ""; + for (var idx = 0; idx < aIdentifier.length; idx++) { + if (msg != "") { + msg += ", "; + } + + msg += prettyName(aIdentifier[idx]); + } + return msg; + } + + if (aIdentifier instanceof nsIAccessible) { + var acc = getAccessible(aIdentifier); + var domID = getAccessibleDOMNodeID(acc); + let msg = "["; + try { + if (Services.appinfo.browserTabsRemoteAutostart) { + if (domID) { + msg += `DOM node id: ${domID}, `; + } + } else { + msg += `${getNodePrettyName(acc.DOMNode)}, `; + } + msg += "role: " + roleToString(acc.role); + if (acc.name) { + msg += ", name: '" + shortenString(acc.name) + "'"; + } + } catch (e) { + msg += "defunct"; + } + + if (acc) { + msg += ", address: " + getObjAddress(acc); + } + msg += "]"; + + return msg; + } + + if (Node.isInstance(aIdentifier)) { + return "[ " + getNodePrettyName(aIdentifier) + " ]"; + } + + if (aIdentifier && typeof aIdentifier === "object") { + var treeObj = normalizeAccTreeObj(aIdentifier); + if ("role" in treeObj) { + function stringifyTree(aObj) { + var text = roleToString(aObj.role) + ": [ "; + if ("children" in aObj) { + for (var i = 0; i < aObj.children.length; i++) { + var c = normalizeAccTreeObj(aObj.children[i]); + text += stringifyTree(c); + if (i < aObj.children.length - 1) { + text += ", "; + } + } + } + return text + "] "; + } + return `{ ${stringifyTree(treeObj)} }`; + } + return JSON.stringify(aIdentifier); + } + + return " '" + aIdentifier + "' "; +} + +/** + * Shorten a long string if it exceeds MAX_TRIM_LENGTH. + * @param aString the string to shorten. + * @returns the shortened string. + */ +function shortenString(aString, aMaxLength) { + if (aString.length <= MAX_TRIM_LENGTH) { + return aString; + } + + // Trim the string if its length is > MAX_TRIM_LENGTH characters. + var trimOffset = MAX_TRIM_LENGTH / 2; + return ( + aString.substring(0, trimOffset - 1) + + "..." + + aString.substring(aString.length - trimOffset, aString.length) + ); +} + +// ////////////////////////////////////////////////////////////////////////////// +// General Utils +// ////////////////////////////////////////////////////////////////////////////// +/** + * Return main chrome window (crosses chrome boundary) + */ +function getMainChromeWindow(aWindow) { + return aWindow.browsingContext.topChromeWindow; +} + +/** Sets the test plugin(s) initially expected enabled state. + * It will automatically be reset to it's previous value after the test + * ends. + * @param aNewEnabledState [in] the enabled state, e.g. SpecialPowers.Ci.nsIPluginTag.STATE_ENABLED + * @param aPluginName [in, optional] The name of the plugin, defaults to "Test Plug-in" + */ +function setTestPluginEnabledState(aNewEnabledState, aPluginName) { + var plugin = getTestPluginTag(aPluginName); + var oldEnabledState = plugin.enabledState; + plugin.enabledState = aNewEnabledState; + SimpleTest.registerCleanupFunction(function() { + getTestPluginTag(aPluginName).enabledState = oldEnabledState; + }); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private +// ////////////////////////////////////////////////////////////////////////////// + +// ////////////////////////////////////////////////////////////////////////////// +// Accessible general + +function getNodePrettyName(aNode) { + try { + var tag = ""; + if (aNode.nodeType == Node.DOCUMENT_NODE) { + tag = "document"; + } else { + tag = aNode.localName; + if (aNode.nodeType == Node.ELEMENT_NODE && aNode.hasAttribute("id")) { + tag += '@id="' + aNode.getAttribute("id") + '"'; + } + } + + return "'" + tag + " node', address: " + getObjAddress(aNode); + } catch (e) { + return "' no node info '"; + } +} + +function getObjAddress(aObj) { + var exp = /native\s*@\s*(0x[a-f0-9]+)/g; + var match = exp.exec(aObj.toString()); + if (match) { + return match[1]; + } + + return aObj.toString(); +} + +function getTestPluginTag(aPluginName) { + var ph = SpecialPowers.Cc["@mozilla.org/plugin/host;1"].getService( + SpecialPowers.Ci.nsIPluginHost + ); + var tags = ph.getPluginTags(); + var name = aPluginName || "Test Plug-in"; + for (var tag of tags) { + if (tag.name == name) { + return tag; + } + } + + ok(false, "Could not find plugin tag with plugin name '" + name + "'"); + return null; +} + +function normalizeAccTreeObj(aObj) { + var key = Object.keys(aObj)[0]; + var roleName = "ROLE_" + key; + if (roleName in nsIAccessibleRole) { + return { + role: nsIAccessibleRole[roleName], + children: aObj[key], + }; + } + return aObj; +} diff --git a/accessible/tests/mochitest/dumbfile.zip b/accessible/tests/mochitest/dumbfile.zip Binary files differnew file mode 100644 index 0000000000..15cb0ecb3e --- /dev/null +++ b/accessible/tests/mochitest/dumbfile.zip diff --git a/accessible/tests/mochitest/editabletext/a11y.ini b/accessible/tests/mochitest/editabletext/a11y.ini new file mode 100644 index 0000000000..68466fdf23 --- /dev/null +++ b/accessible/tests/mochitest/editabletext/a11y.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + editabletext.js + !/accessible/tests/mochitest/*.js + +[test_1.html] +[test_2.html] diff --git a/accessible/tests/mochitest/editabletext/editabletext.js b/accessible/tests/mochitest/editabletext/editabletext.js new file mode 100644 index 0000000000..ef305c7842 --- /dev/null +++ b/accessible/tests/mochitest/editabletext/editabletext.js @@ -0,0 +1,409 @@ +/* import-globals-from ../common.js */ +/* import-globals-from ../events.js */ + +/** + * Perform all editable text tests. + */ +function editableTextTestRun() { + this.add = function add(aTest) { + this.seq.push(aTest); + }; + + this.run = function run() { + this.iterate(); + }; + + this.index = 0; + this.seq = []; + + this.iterate = function iterate() { + if (this.index < this.seq.length) { + this.seq[this.index++].startTest(this); + return; + } + + this.seq = null; + SimpleTest.finish(); + }; +} + +/** + * Used to test nsIEditableTextAccessible methods. + */ +function editableTextTest(aID) { + /** + * Schedule a test, the given function with its arguments will be executed + * when preceding test is complete. + */ + this.scheduleTest = function scheduleTest(aFunc, ...aFuncArgs) { + // A data container acts like a dummy invoker, it's never invoked but + // it's used to generate real invoker when previous invoker was handled. + var dataContainer = { + func: aFunc, + funcArgs: aFuncArgs, + }; + this.mEventQueue.push(dataContainer); + + if (!this.mEventQueueReady) { + this.unwrapNextTest(); + this.mEventQueueReady = true; + } + }; + + /** + * setTextContents test. + */ + this.setTextContents = function setTextContents(aValue, aSkipStartOffset) { + var testID = "setTextContents '" + aValue + "' for " + prettyName(aID); + + function setTextContentsInvoke() { + dump(`\nsetTextContents '${aValue}'\n`); + var acc = getAccessible(aID, nsIAccessibleEditableText); + acc.setTextContents(aValue); + } + + aSkipStartOffset = aSkipStartOffset || 0; + var insertTripple = aValue + ? [aSkipStartOffset, aSkipStartOffset + aValue.length, aValue] + : null; + var oldValue = getValue(); + var removeTripple = oldValue + ? [aSkipStartOffset, aSkipStartOffset + oldValue.length, oldValue] + : null; + + this.generateTest( + removeTripple, + insertTripple, + setTextContentsInvoke, + getValueChecker(aValue), + testID + ); + }; + + /** + * insertText test. + */ + this.insertText = function insertText(aStr, aPos, aResStr, aResPos) { + var testID = + "insertText '" + aStr + "' at " + aPos + " for " + prettyName(aID); + + function insertTextInvoke() { + dump(`\ninsertText '${aStr}' at ${aPos} pos\n`); + var acc = getAccessible(aID, nsIAccessibleEditableText); + acc.insertText(aStr, aPos); + } + + var resPos = aResPos != undefined ? aResPos : aPos; + this.generateTest( + null, + [resPos, resPos + aStr.length, aStr], + insertTextInvoke, + getValueChecker(aResStr), + testID + ); + }; + + /** + * copyText test. + */ + this.copyText = function copyText(aStartPos, aEndPos, aClipboardStr) { + var testID = + "copyText from " + + aStartPos + + " to " + + aEndPos + + " for " + + prettyName(aID); + + function copyTextInvoke() { + var acc = getAccessible(aID, nsIAccessibleEditableText); + acc.copyText(aStartPos, aEndPos); + } + + this.generateTest( + null, + null, + copyTextInvoke, + getClipboardChecker(aClipboardStr), + testID + ); + }; + + /** + * copyText and pasteText test. + */ + this.copyNPasteText = function copyNPasteText( + aStartPos, + aEndPos, + aPos, + aResStr + ) { + var testID = + "copyText from " + + aStartPos + + " to " + + aEndPos + + "and pasteText at " + + aPos + + " for " + + prettyName(aID); + + function copyNPasteTextInvoke() { + var acc = getAccessible(aID, nsIAccessibleEditableText); + acc.copyText(aStartPos, aEndPos); + acc.pasteText(aPos); + } + + this.generateTest( + null, + [aStartPos, aEndPos, getTextFromClipboard], + copyNPasteTextInvoke, + getValueChecker(aResStr), + testID + ); + }; + + /** + * cutText test. + */ + this.cutText = function cutText( + aStartPos, + aEndPos, + aResStr, + aResStartPos, + aResEndPos + ) { + var testID = + "cutText from " + + aStartPos + + " to " + + aEndPos + + " for " + + prettyName(aID); + + function cutTextInvoke() { + var acc = getAccessible(aID, nsIAccessibleEditableText); + acc.cutText(aStartPos, aEndPos); + } + + var resStartPos = aResStartPos != undefined ? aResStartPos : aStartPos; + var resEndPos = aResEndPos != undefined ? aResEndPos : aEndPos; + this.generateTest( + [resStartPos, resEndPos, getTextFromClipboard], + null, + cutTextInvoke, + getValueChecker(aResStr), + testID + ); + }; + + /** + * cutText and pasteText test. + */ + this.cutNPasteText = function copyNPasteText( + aStartPos, + aEndPos, + aPos, + aResStr + ) { + var testID = + "cutText from " + + aStartPos + + " to " + + aEndPos + + " and pasteText at " + + aPos + + " for " + + prettyName(aID); + + function cutNPasteTextInvoke() { + var acc = getAccessible(aID, nsIAccessibleEditableText); + acc.cutText(aStartPos, aEndPos); + acc.pasteText(aPos); + } + + this.generateTest( + [aStartPos, aEndPos, getTextFromClipboard], + [aPos, -1, getTextFromClipboard], + cutNPasteTextInvoke, + getValueChecker(aResStr), + testID + ); + }; + + /** + * pasteText test. + */ + this.pasteText = function pasteText(aPos, aResStr) { + var testID = "pasteText at " + aPos + " for " + prettyName(aID); + + function pasteTextInvoke() { + var acc = getAccessible(aID, nsIAccessibleEditableText); + acc.pasteText(aPos); + } + + this.generateTest( + null, + [aPos, -1, getTextFromClipboard], + pasteTextInvoke, + getValueChecker(aResStr), + testID + ); + }; + + /** + * deleteText test. + */ + this.deleteText = function deleteText(aStartPos, aEndPos, aResStr) { + var testID = + "deleteText from " + + aStartPos + + " to " + + aEndPos + + " for " + + prettyName(aID); + + var oldValue = getValue().substring(aStartPos, aEndPos); + var removeTripple = oldValue ? [aStartPos, aEndPos, oldValue] : null; + + function deleteTextInvoke() { + var acc = getAccessible(aID, [nsIAccessibleEditableText]); + acc.deleteText(aStartPos, aEndPos); + } + + this.generateTest( + removeTripple, + null, + deleteTextInvoke, + getValueChecker(aResStr), + testID + ); + }; + + // //////////////////////////////////////////////////////////////////////////// + // Implementation details. + + function getValue() { + var elm = getNode(aID); + var elmClass = ChromeUtils.getClassName(elm); + if (elmClass === "HTMLTextAreaElement" || elmClass === "HTMLInputElement") { + return elm.value; + } + + if (elmClass === "HTMLDocument") { + return elm.body.textContent; + } + + return elm.textContent; + } + + /** + * Common checkers. + */ + function getValueChecker(aValue) { + var checker = { + check: function valueChecker_check() { + is(getValue(), aValue, "Wrong value " + aValue); + }, + }; + return checker; + } + + function getClipboardChecker(aText) { + var checker = { + check: function clipboardChecker_check() { + is(getTextFromClipboard(), aText, "Wrong text in clipboard."); + }, + }; + return checker; + } + + /** + * Process next scheduled test. + */ + this.unwrapNextTest = function unwrapNextTest() { + var data = this.mEventQueue.mInvokers[this.mEventQueue.mIndex + 1]; + if (data) { + data.func.apply(this, data.funcArgs); + } + }; + + /** + * Used to generate an invoker object for the sheduled test. + */ + this.generateTest = function generateTest( + aRemoveTriple, + aInsertTriple, + aInvokeFunc, + aChecker, + aInvokerID + ) { + var et = this; + var invoker = { + eventSeq: [], + + invoke: aInvokeFunc, + finalCheck: function finalCheck() { + // dumpTree(aID, `'${aID}' tree:`); + + aChecker.check(); + et.unwrapNextTest(); // replace dummy invoker on real invoker object. + }, + getID: function getID() { + return aInvokerID; + }, + }; + + if (aRemoveTriple) { + let checker = new textChangeChecker( + aID, + aRemoveTriple[0], + aRemoveTriple[1], + aRemoveTriple[2], + false + ); + invoker.eventSeq.push(checker); + } + + if (aInsertTriple) { + let checker = new textChangeChecker( + aID, + aInsertTriple[0], + aInsertTriple[1], + aInsertTriple[2], + true + ); + invoker.eventSeq.push(checker); + } + + // Claim that we don't want to fail when no events are expected. + if (!aRemoveTriple && !aInsertTriple) { + invoker.noEventsOnAction = true; + } + + this.mEventQueue.mInvokers[this.mEventQueue.mIndex + 1] = invoker; + }; + + /** + * Run the tests. + */ + this.startTest = function startTest(aTestRun) { + var testRunObj = aTestRun; + var thisObj = this; + this.mEventQueue.onFinish = function finishCallback() { + // Notify textRun object that all tests were finished. + testRunObj.iterate(); + + // Help GC to avoid leaks (refer to aTestRun from local variable, drop + // onFinish function). + thisObj.mEventQueue.onFinish = null; + + return DO_NOT_FINISH_TEST; + }; + + this.mEventQueue.invoke(); + }; + + this.mEventQueue = new eventQueue(); + this.mEventQueueReady = false; +} diff --git a/accessible/tests/mochitest/editabletext/test_1.html b/accessible/tests/mochitest/editabletext/test_1.html new file mode 100644 index 0000000000..10b5719374 --- /dev/null +++ b/accessible/tests/mochitest/editabletext/test_1.html @@ -0,0 +1,140 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=452161 +--> +<head> + <title>nsIAccessibleEditableText chrome tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="editabletext.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + // enableLogging("tree,verbose"); // debug + + function addTestEditable(aID, aTestRun, aBeforeContent, aAfterContent) { + var et = new editableTextTest(aID); + var startOffset = aBeforeContent ? aBeforeContent.length : 0; + // XXX afterContent currently is not used + + // //////////////////////////////////////////////////////////////////////// + // setTextContents + et.scheduleTest(et.setTextContents, "hello", startOffset); + et.scheduleTest(et.setTextContents, "olleh", startOffset); + et.scheduleTest(et.setTextContents, "", startOffset); + + // //////////////////////////////////////////////////////////////////////// + // insertText + et.scheduleTest(et.insertText, "hello", startOffset, "hello"); + et.scheduleTest(et.insertText, "ma ", startOffset, "ma hello"); + et.scheduleTest(et.insertText, "ma", startOffset + 2, "mama hello"); + et.scheduleTest(et.insertText, " hello", startOffset + 10, "mama hello hello"); + + // XXX: bug 452584 + + // //////////////////////////////////////////////////////////////////////// + // deleteText +// et.deleteText(0, 5, "hello hello"); +// et.deleteText(5, 6, "hellohello"); +// et.deleteText(5, 10, "hello"); +// et.deleteText(0, 5, ""); + + // //////////////////////////////////////////////////////////////////////// + // copyNPasteText +// et.copyNPasteText(0, 0, 0, ""); +// et.insertText("hello", 0, "hello"); +// et.copyNPasteText(0, 1, 0, "hhello"); +// et.copyNPasteText(5, 6, 6, "hhelloo"); +// et.copyNPasteText(3, 4, 1, "hehelloo"); + + // //////////////////////////////////////////////////////////////////////// +// // cutNPasteText +// et.cutNPasteText(0, 1, 1, "ehhelloo"); +// et.cutNPasteText(1, 2, 0, "hehelloo"); +// et.cutNPasteText(7, 8, 8, "hehelloo"); + + aTestRun.add(et); + } + + // gA11yEventDumpToConsole = true; // debug stuff + + function runTest() { + var testRun = new editableTextTestRun(); + + addTestEditable("input", testRun); + addTestEditable("div", testRun); + addTestEditable("divb", testRun, "pseudo element", ""); + addTestEditable("diva", testRun, "", "pseudo element"); + addTestEditable("divba", testRun, "before", "after"); + addTestEditable(getNode("frame").contentDocument, testRun); + + testRun.run(); // Will call SimpleTest.finish(); + } + + function doTest() { + // Prepare tested elements. + + // Design mode on/off triggers an editable state change event on + // the document accessible. + var frame = getNode("frame"); + waitForEvent(EVENT_STATE_CHANGE, frame.contentDocument, runTest); + frame.contentDocument.designMode = "on"; + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + <style> + #divb::before, + #diva::after { + content: "pseudo element"; + } + #divba::before { + content: "before"; + } + #divba::after { + content: "after"; + } + </style> +</head> +<body> + + <a target="_blank" + title="nsIAccessibleEditableText chrome tests" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=452161"> + Bug 452161 + </a> + <a target="_blank" + title="Cache rendered text on a11y side" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660"> + Bug 626660 + </a> + <a target="_blank" + title="Pseudo element support test" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1105611"> + Bug 1105611 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input"/> + + <div id="div" contenteditable="true"></div> + <div id="divb" contenteditable="true"></div> + <div id="diva" contenteditable="true"></div> + <div id="divba" contenteditable="true"></div> + + <iframe id="frame"/> +</body> +</html> diff --git a/accessible/tests/mochitest/editabletext/test_2.html b/accessible/tests/mochitest/editabletext/test_2.html new file mode 100644 index 0000000000..1d252d836f --- /dev/null +++ b/accessible/tests/mochitest/editabletext/test_2.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessibleEditableText chrome tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="editabletext.js"></script> + + <script type="application/javascript"> + function doTest() { + var et = new editableTextTest("input"); + + // 'ee' insertion/removal at 1 or 2 offset of 'hello'/'heeello' string + // reports 'ee' text was inserted/removed at 2 offset. + et.scheduleTest(et.insertText, "ee", 1, "heeello", 2); + et.scheduleTest(et.copyText, 1, 3, "ee"); + et.scheduleTest(et.cutText, 1, 3, "hello", 2, 4); + et.scheduleTest(et.insertText, "ee", 2, "heeello", 2); + et.scheduleTest(et.cutText, 2, 4, "hello", 2, 4); + + et.scheduleTest(et.deleteText, 1, 3, "hlo"); + et.scheduleTest(et.pasteText, 1, "heelo"); + + var testRun = new editableTextTestRun(); + testRun.add(et); + testRun.run(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="HyperText accessible should get focus when the caret is positioned inside of it, text is changed or copied into clipboard by ATs" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=524115"> + Mozilla Bug 524115 + </a> + <a target="_blank" + title="Cache rendered text on a11y side" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660"> + Mozilla Bug 626660 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input" value="hello"/> + +</body> +</html> diff --git a/accessible/tests/mochitest/elm/a11y.ini b/accessible/tests/mochitest/elm/a11y.ini new file mode 100644 index 0000000000..2b3cf492ed --- /dev/null +++ b/accessible/tests/mochitest/elm/a11y.ini @@ -0,0 +1,15 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/moz.png + !/dom/media/test/bug461281.ogg + !/dom/security/test/csp/dummy.pdf + +[test_HTMLSpec.html] +[test_figure.html] +[test_listbox.xhtml] +[test_MathMLSpec.html] +[test_nsApplicationAcc.html] +[test_canvas.html] +[test_shadowroot.html] +support-files = test_shadowroot_subframe.html diff --git a/accessible/tests/mochitest/elm/test_HTMLSpec.html b/accessible/tests/mochitest/elm/test_HTMLSpec.html new file mode 100644 index 0000000000..ad8170f090 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_HTMLSpec.html @@ -0,0 +1,1972 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML a11y spec tests</title> + <link id="link" rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <script type="application/javascript"> + async function doTest() { + // //////////////////////////////////////////////////////////////////////// + // HTML:a@href + + var obj = { + role: ROLE_LINK, + states: STATE_LINKED, + actions: "jump", + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText, nsIAccessibleHyperLink ], + children: [ // all kids inherits linked state and jump action + { + role: ROLE_TEXT_LEAF, + states: STATE_LINKED, + actions: "jump", + }, + ], + }; + testElm("a_href", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:a no @href + + obj = { + todo_role: ROLE_TEXT_CONTAINER, + absentStates: STATE_LINKED, + actions: null, + children: [ + { + role: ROLE_TEXT_LEAF, + absentStates: STATE_LINKED, + actions: null, + }, + ], + }; + testElm("a_nohref", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:abbr contained by HTML:td + + obj = { + role: ROLE_CELL, + attributes: { abbr: "WWW" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + children: [ + { + role: ROLE_TEXT, + children: [ { role: ROLE_TEXT_LEAF } ], + }, + ], + }; + testElm("td_abbr", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:address + + obj = { + role: ROLE_TEXT_CONTAINER, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("address", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:area@href + + obj = { + role: ROLE_LINK, + states: STATE_LINKED, + actions: "jump", + interfaces: [ nsIAccessibleHyperLink ], + children: [], + }; + testElm(getAccessible("imgmap").firstChild, obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:area no @href + + obj = { + todo_role: "ROLE_SHAPE", + absentStates: STATE_LINKED, + children: [], + }; + testElm(getAccessible("imgmap").lastChild, obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:article + obj = { + role: ROLE_ARTICLE, + states: STATE_READONLY, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("article", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:aside + obj = { + role: ROLE_LANDMARK, + attributes: { "xml-roles": "complementary" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("aside", obj); + + // //////////////////////////////////////////////////////////////////////// + obj = { // HTML:audio + role: ROLE_GROUPING, + }; + testElm("audio", obj); + + // //////////////////////////////////////////////////////////////////////// + obj = { // HTML:b contained by paragraph + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "font-weight": kBoldFontWeight }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:b text + ], + }; + testElm("b_container", obj); + + // //////////////////////////////////////////////////////////////////////// + obj = { // HTML:bdi contained by paragraph + role: ROLE_PARAGRAPH, + todo_textAttrs: { + 0: { }, + 5: { "writing-mode": "rl" }, + 8: { }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:bdi text + { role: ROLE_TEXT_LEAF }, // plain text + ], + }; + testElm("bdi_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:bdo contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + todo_textAttrs: { + 0: { }, + 6: { "writing-mode": "rl" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + ], + }; + testElm("bdo_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:blockquote + + obj = { + role: ROLE_BLOCKQUOTE, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + children: [ { role: ROLE_PARAGRAPH } ], + }; + testElm("blockquote", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:br contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + children: [ + { role: ROLE_WHITESPACE }, + { role: ROLE_WHITESPACE } + ] + }; + testElm("br_container", obj); + + // //////////////////////////////////////////////////////////////////////// + obj = { // HTML:button + role: ROLE_PUSHBUTTON, + absentStates: STATE_DEFAULT, + actions: "press", + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("button", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:button@type="submit" (default button) + + obj = { + role: ROLE_PUSHBUTTON, + states: STATE_DEFAULT, + actions: "press", + }; + testElm("button_default", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:canvas + + obj = { + role: ROLE_CANVAS, + }; + testElm("canvas", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:caption under table + + obj = { + role: ROLE_TABLE, + relations: { + RELATION_LABELLED_BY: "caption", + }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText, nsIAccessibleTable ], + children: [ + { + role: ROLE_CAPTION, + relations: { + RELATION_LABEL_FOR: "table", + }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }, + { // td inside thead + role: ROLE_ROW, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + children: [ + { + role: ROLE_COLUMNHEADER, + interfaces: [ nsIAccessibleTableCell, nsIAccessibleText, nsIAccessibleHyperText ], + }, + { role: ROLE_COLUMNHEADER }, + ], + }, + { // td inside tbody + role: ROLE_ROW, + children: [ + { + role: ROLE_ROWHEADER, + interfaces: [ nsIAccessibleTableCell, nsIAccessibleText, nsIAccessibleHyperText ], + }, + { + role: ROLE_CELL, + interfaces: [ nsIAccessibleTableCell, nsIAccessibleText, nsIAccessibleHyperText ], + }, + ], + }, + { // td inside tfoot + role: ROLE_ROW, + }, + ], + }; + testElm("table", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:cite contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "font-style": "italic" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:cite text + ], + }; + testElm("cite_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:code contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "font-family": kMonospaceFontFamily }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:code text + ], + }; + testElm("code_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:col and HTML:colgroup under table + + obj = + { TABLE: [ + { ROW: [ + { role: ROLE_CELL }, + { role: ROLE_CELL }, + { role: ROLE_CELL }, + ] }, + ] }; + testElm("colNcolgroup_table", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:data contained by paragraph + + obj = + { PARAGRAPH: [ + { TEXT_LEAF: [] }, // HTML:data text + ] }; + testElm("data_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:datalist associated with input + + todo(false, "datalist and summary tree hierarchy test missed"); + + // //////////////////////////////////////////////////////////////////////// + // HTML:dd, HTML:dl, HTML:dd + + obj = { + role: ROLE_DEFINITION_LIST, + states: STATE_READONLY, + children: [ // dl + { + role: ROLE_TERM, + states: STATE_READONLY, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + children: [ // dt + { role: ROLE_TEXT_LEAF }, + ], + }, + { + role: ROLE_DEFINITION, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + children: [ // dd + { role: ROLE_TEXT_LEAF }, + ], + }, + ], + }; + testElm("dl", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:del contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_CONTENT_DELETION }, + ], + }; + testElm("del_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:details with open state + + obj = { + role: ROLE_DETAILS, + children: [ + { + role: ROLE_SUMMARY, + states: STATE_EXPANDED, + actions: "collapse", + }, + { role: ROLE_PARAGRAPH }, + ], + }; + testElm("details", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:details with closed (default) state + + obj = { + role: ROLE_DETAILS, + children: [ + { + role: ROLE_SUMMARY, + states: STATE_COLLAPSED, + actions: "expand", + }, + ], + }; + testElm("details_closed", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:dfn contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { "font-style": "italic" }, + 12: { }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // HTML:dfn text + { role: ROLE_TEXT_LEAF }, // plain text + ], + }; + testElm("dfn_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:dialog + + // XXX: Remove the pushing of the pref and just run the test once the + // dialog element is enabled by default. + await SpecialPowers.pushPrefEnv({ set: [["dom.dialog_element.enabled", true]] }); + obj = { + role: ROLE_DIALOG, + children: [ + { role: ROLE_TEXT_LEAF }, + ], + }; + testElm("dialog", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:div + + obj = { + role: ROLE_SECTION, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + ], + }; + testElm("div", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:em in a paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "font-style": "italic" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:em text + ], + }; + testElm("em_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:embed (windowless/windowed plugins and media) + + ok(!isAccessible("embed_plugin_windowless"), "(blocked) windowless plugin embed element is not accessible"); + ok(!isAccessible("embed_plugin_windowed"), "(blocked) windowed plugin embed element is not accessible"); + + obj = { + role: ROLE_GRAPHIC, + interfaces: [ nsIAccessibleImage ], + }; + testElm("embed_png", obj); + + obj = { + INTERNAL_FRAME: [ { + DOCUMENT: [ { + role: ROLE_PARAGRAPH, + } ], + } ], + }; + testElm("embed_html", obj); + + obj = { + INTERNAL_FRAME: [ { + DOCUMENT: [ { + } ], + } ], + }; + testElm("embed_pdf", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:fieldset and HTML:legend + + obj = { + role: ROLE_GROUPING, + name: "legend", + relations: { + RELATION_LABELLED_BY: "legend", + }, + children: [ + { + role: ROLE_LABEL, + name: "legend", + relations: { + RELATION_LABEL_FOR: "fieldset", + }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }, + { + role: ROLE_ENTRY, + }, + ], + }; + testElm("fieldset", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:figure and HTML:figcaption + + obj = { + role: ROLE_FIGURE, + attributes: { "xml-roles": "figure" }, + relations: { + RELATION_LABELLED_BY: "figcaption", + }, + children: [ + { role: ROLE_GRAPHIC }, + { + role: ROLE_CAPTION, + relations: { + RELATION_LABEL_FOR: "figure", + }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }, + ], + }; + testElm("figure", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:footer + + obj = { + role: ROLE_LANDMARK, + attributes: { "xml-roles": "contentinfo" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("footer", obj); + + obj = { + role: ROLE_SECTION, + absentAttributes: { "xml-roles": "contentinfo" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("footer_in_article", obj); + testElm("footer_in_aside", obj); + testElm("footer_in_main", obj); + testElm("footer_in_nav", obj); + testElm("footer_in_section", obj); + testElm("footer_in_blockquote", obj); + testElm("footer_in_details", obj); + testElm("footer_in_dialog", obj); + testElm("footer_in_fieldset", obj); + testElm("footer_in_figure", obj); + testElm("footer_in_td", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:form + + obj = { + role: ROLE_FORM, + absentAttributes: { "xml-roles": "form" }, + }; + testElm("form", obj); + + // HTML:form with an accessible name + + obj = { + role: ROLE_FORM_LANDMARK, + attributes: { "xml-roles": "form" }, + }; + testElm("named_form", obj); + + // //////////////////////////////////////////////////////////////////////// + // // HTML:frameset, HTML:frame and HTML:iframe + + obj = { + INTERNAL_FRAME: [ { // HTML:iframe + DOCUMENT: [ { + INTERNAL_FRAME: [ { // HTML:frame + DOCUMENT: [ { role: ROLE_TEXT_LEAF} ], + } ], + } ], + } ], + }; + testElm("frameset_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:h1, HTML:h2, HTML:h3, HTML:h4, HTML:h5, HTML:h6 + + function headingWithLevel(i) { + return { + role: ROLE_HEADING, + attributes: { "level": i.toString() }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + } + + for (let level = 1; level <= 6; ++level) { + testElm("h" + level, headingWithLevel(level)); + for (const ancestor of ["section", "article", "aside", "nav"]) { + testElm("h" + level + "_in_" + ancestor, headingWithLevel(level)); + testElm("h" + level + "_in_" + ancestor + "_in_hgroup", headingWithLevel(level)); + } + } + + // //////////////////////////////////////////////////////////////////////// + // HTML:header + + obj = { + role: ROLE_LANDMARK, + attributes: { "xml-roles": "banner" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("header", obj); + + obj = { + role: ROLE_SECTION, + absentAttributes: { "xml-roles": "banner" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("header_in_article", obj); + testElm("header_in_aside", obj); + testElm("header_in_main", obj); + testElm("header_in_nav", obj); + testElm("header_in_section", obj); + testElm("header_in_blockquote", obj); + testElm("header_in_details", obj); + testElm("header_in_dialog", obj); + testElm("header_in_fieldset", obj); + testElm("header_in_figure", obj); + testElm("header_in_td", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:hr + + obj = { + role: ROLE_SEPARATOR, + }; + testElm("hr", obj); + + // //////////////////////////////////////////////////////////////////////// + obj = { // HTML:i contained by paragraph + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "font-style": "italic" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:i text + ], + }; + testElm("i_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:img + + obj = { + role: ROLE_GRAPHIC, + interfaces: [ nsIAccessibleImage ], + }; + testElm("img", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="button" + + obj = { + role: ROLE_PUSHBUTTON, + absentStates: STATE_DEFAULT, + }; + testElm("input_button", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="checkbox" + + obj = { + role: ROLE_CHECKBUTTON, + states: STATE_CHECKABLE, + absentStates: STATE_CHECKED, + actions: "check", + }; + testElm("input_checkbox", obj); + + obj = { + role: ROLE_CHECKBUTTON, + states: STATE_CHECKABLE | STATE_CHECKED, + actions: "uncheck", + }; + testElm("input_checkbox_true", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="file" + + obj = { + GROUPING: [ + { role: ROLE_PUSHBUTTON }, + { role: ROLE_LABEL }, + ], + }; + testElm("input_file", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="image" + + obj = { + role: ROLE_PUSHBUTTON, + absentStates: STATE_DEFAULT, + actions: "press", + }; + testElm("input_image", obj); + testElm("input_image_display", obj); + testElm("input_submit", obj); + + obj = { + role: ROLE_PUSHBUTTON, + actions: "press", + states: STATE_DEFAULT, + }; + testElm("input_image_default", obj); + testElm("input_submit_default", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="number" and etc + + obj = { + role: ROLE_SPINBUTTON, + interfaces: [ nsIAccessibleValue, nsIAccessibleText, nsIAccessibleEditableText ], + children: [ + { role: ROLE_TEXT_LEAF }, + ], + }; + testElm("input_number", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="text" and etc + + obj = { + role: ROLE_ENTRY, + extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE, + actions: "activate", + interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ], + children: [ + { role: ROLE_TEXT_LEAF }, + ], + }; + testElm("input_email", obj); + testElm("input_search", obj); + testElm("input_tel", obj); + testElm("input_text", obj); + testElm("input_url", obj); + + // //////////////////////////////////////////////////////////////////////// + // input @type="text" with placeholder attribute + + // First: Label and placeholder, text is the same, no attribute. + obj = { + role: ROLE_ENTRY, + name: "Your name", + extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE, + actions: "activate", + absentAttributes: { placeholder: "Your name" }, + interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ], + children: [], + }; + testElm("input_placeholder_same", obj); + + // Second: Label and placeholder, text is different, attribute. + obj = { + role: ROLE_ENTRY, + name: "First name:", + extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE, + actions: "activate", + attributes: { placeholder: "Enter your first name" }, + interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ], + children: [], + }; + testElm("input_placeholder_different", obj); + + // Third: placeholder only, text is name, no attribute. + obj = { + role: ROLE_ENTRY, + name: "Date of birth", + extraStates: EXT_STATE_EDITABLE | EXT_STATE_SINGLE_LINE, + actions: "activate", + absentAttributes: { placeholder: "Date of birth" }, + interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ], + children: [], + }; + testElm("input_placeholder_only", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="password" + + obj = { + role: ROLE_PASSWORD_TEXT, + states: STATE_PROTECTED, + extraStates: EXT_STATE_EDITABLE, + actions: "activate", + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }; + testElm("input_password", obj); + ok(getAccessible("input_password").firstChild.name != "44", + "text leaf for password shouldn't have its real value as its name!"); + + obj = { + role: ROLE_PASSWORD_TEXT, + states: STATE_PROTECTED | STATE_READONLY, + actions: "activate", + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }; + testElm("input_password_readonly", obj); + ok(getAccessible("input_password_readonly").firstChild.name != "44", + "text leaf for password shouldn't have its real value as its name!"); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="radio" + + obj = { + role: ROLE_RADIOBUTTON, + states: STATE_CHECKABLE, + absentStates: STATE_CHECKED, + actions: "select", + }; + testElm("input_radio", obj); + + obj = { + role: ROLE_RADIOBUTTON, + states: STATE_CHECKABLE | STATE_CHECKED, + actions: "select", + }; + testElm("input_radio_true", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="range" + + obj = { + role: ROLE_SLIDER, + }; + testElm("input_range", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="reset" + + obj = { + role: ROLE_PUSHBUTTON, + actions: "press", + absentStates: STATE_DEFAULT, + }; + testElm("input_reset", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="time" + + obj = { + role: ROLE_TIME_EDITOR, + name: "time label", + attributes: { "text-input-type": "time" }, + children: [ + { role: ROLE_SPINBUTTON }, + { role: ROLE_TEXT_LEAF }, + { role: ROLE_SPINBUTTON }, + { role: ROLE_TEXT_LEAF }, + { role: ROLE_ENTRY }, + { role: ROLE_PUSHBUTTON }, + ], + }; + testElm("input_time", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:input@type="date" + + obj = { + role: ROLE_DATE_EDITOR, + name: "date label", + attributes: { "text-input-type": "date" }, + children: [ + { role: ROLE_SPINBUTTON }, + { role: ROLE_TEXT_LEAF }, + { role: ROLE_SPINBUTTON }, + { role: ROLE_TEXT_LEAF }, + { role: ROLE_SPINBUTTON }, + { role: ROLE_PUSHBUTTON }, + ], + }; + testElm("input_date", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:ins contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_CONTENT_INSERTION }, + ], + }; + testElm("ins_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:kbd contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "font-family": kMonospaceFontFamily }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:kbd text + ], + }; + testElm("kbd_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:label + + obj = { + role: ROLE_LABEL, + todo_relations: { + RELATION_LABEL_FOR: "label_input", + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { + role: ROLE_ENTRY, + relations: { + RELATION_LABELLED_BY: "label", + }, + }, + ], + }; + testElm("label", obj); + + obj = { + role: ROLE_LABEL, + relations: { + RELATION_LABEL_FOR: "label_for_input", + }, + }; + testElm("label_for", obj); + + obj = { + role: ROLE_ENTRY, + relations: { + RELATION_LABELLED_BY: "label_for", + }, + }; + testElm("label_for_input", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:ul, HTML:ol, HTML:li + + obj = { // ul or ol + role: ROLE_LIST, + states: STATE_READONLY, + children: [ + { // li + role: ROLE_LISTITEM, + states: STATE_READONLY, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }, + ], + }; + testElm("ul", obj); + testElm("ol", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:link + + ok(!isAccessible("link"), "link element is not accessible"); + + // //////////////////////////////////////////////////////////////////////// + // HTML:main + + obj = { + role: ROLE_LANDMARK, + attributes: { "xml-roles": "main" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("main", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:map + + ok(!isAccessible("map_imagemap"), + "map element is not accessible if used as an image map"); + + obj = { + role: ROLE_TEXT_CONTAINER, + }; + testElm("map", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:mark contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_MARK, // HTML:mark text + attributes: { "xml-roles": "mark" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + textAttrs: { + 0: { }, + } + } + ], + }; + testElm("mark_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:math + + obj = { + role: ROLE_MATHML_MATH, + }; + testElm("math", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:menu + + obj = { + role: ROLE_LIST, // menu + children: [ + { role: ROLE_LISTITEM, + children: [ // home + { role: ROLE_LISTITEM_MARKER }, + { role: ROLE_TEXT_LEAF } + ] + }, + { + role: ROLE_LISTITEM, + children: [ + { role: ROLE_LISTITEM_MARKER }, + { role: ROLE_TEXT_LEAF }, // about + { + role: ROLE_LIST, // menu + children: [ + { role: ROLE_LISTITEM, + children: [ + { role: ROLE_LISTITEM_MARKER }, + { role: ROLE_TEXT_LEAF } // our story + ] + }, + ] + }, + ] + }, + ] + }; + + testElm("menu", obj); + obj = { + role: ROLE_LIST, + children: [ + { + role: ROLE_LISTITEM, + children: [ + { role: ROLE_LISTITEM_MARKER }, + { + role: ROLE_PUSHBUTTON, + children: [ + { role: ROLE_TEXT_LEAF } + ] + }, + { + role: ROLE_LIST, + children: [ + { + role: ROLE_LISTITEM, + children: [ + { role: ROLE_LISTITEM_MARKER }, + { + role: ROLE_PUSHBUTTON, + children: [ + { role: ROLE_TEXT_LEAF } + ] + } + ] + }, + ] + } + ] + } + ] + }; + testElm("menu1", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:meter + + todo(isAccessible("meter"), "meter element is not accessible"); + + // //////////////////////////////////////////////////////////////////////// + // HTML:nav + + obj = { + role: ROLE_LANDMARK, + attributes: { "xml-roles": "navigation" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("nav", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:object (windowless/windowed plugins and media) and HTML:param + + ok(!isAccessible("object_plugin_windowless"), "(blocked) windowless plugin object element is not accessible"); + ok(!isAccessible("object_plugin_windowed"), "(blocked) windowed plugin object element is not accessible"); + + obj = { + role: ROLE_GRAPHIC, + interfaces: [ nsIAccessibleImage ], + }; + testElm("object_png", obj); + + obj = { + INTERNAL_FRAME: [ { + DOCUMENT: [ { + role: ROLE_PARAGRAPH, + } ], + } ], + }; + testElm("object_html", obj); + + obj = { + INTERNAL_FRAME: [ { + DOCUMENT: [ { + } ], + } ], + }; + testElm("object_pdf", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:select, HTML:optgroup and HTML:option + + obj = { // HMTL:select@size > 1 + role: ROLE_LISTBOX, + states: STATE_FOCUSABLE, + absentStates: STATE_MULTISELECTABLE, + interfaces: [ nsIAccessibleSelectable ], + children: [ + { GROUPING: [ // HTML:optgroup + { role: ROLE_STATICTEXT }, + { role: ROLE_OPTION }, // HTML:option + { role: ROLE_OPTION }, + ] }, + { + role: ROLE_OPTION, + states: STATE_FOCUSABLE, + actions: "select", + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }, + ], + }; + testElm("select_listbox", obj); + + obj = { // HTML:select@multiple + role: ROLE_LISTBOX, + states: STATE_FOCUSABLE | STATE_MULTISELECTABLE, + children: [ + { role: ROLE_OPTION }, + { role: ROLE_OPTION }, + { role: ROLE_OPTION }, + ], + }; + testElm("select_listbox_multiselectable", obj); + + obj = { // HTML:select + role: ROLE_COMBOBOX, + states: STATE_FOCUSABLE, + children: [ + { + role: ROLE_COMBOBOX_LIST, + children: [ + { role: ROLE_COMBOBOX_OPTION }, + { role: ROLE_COMBOBOX_OPTION }, + { role: ROLE_COMBOBOX_OPTION }, + ], + }, + ], + }; + testElm("select_combobox", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:output + + obj = { + role: ROLE_STATUSBAR, + attributes: { "live": "polite" }, + todo_relations: { + RELATION_CONTROLLED_BY: "output_input", + }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("output", obj); + + obj = { + role: ROLE_ENTRY, + relations: { + RELATION_CONTROLLER_FOR: "output", + }, + }; + testElm("output_input", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:pre + + obj = { + role: ROLE_TEXT_CONTAINER, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("pre", obj); + + // ///////////////////////////////////////////////////////////////////////// + // HTML:progress + + obj = { + role: ROLE_PROGRESSBAR, + absentStates: STATE_MIXED, + interfaces: [ nsIAccessibleValue ], + }; + testElm("progress", obj); + + obj = { + role: ROLE_PROGRESSBAR, + states: STATE_MIXED, + }; + testElm("progress_indeterminate", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:q + + obj = { + role: ROLE_TEXT, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + children: [ + { role: ROLE_STATICTEXT }, // left quote + { role: ROLE_TEXT_LEAF }, // quoted text + { role: ROLE_STATICTEXT }, // right quote + ], + }; + testElm("q", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:ruby + + todo(isAccessible("ruby"), "ruby element is not accessible"); + + // //////////////////////////////////////////////////////////////////////// + // HTML:s contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "text-line-through-style": "solid" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:i text + ], + }; + testElm("s_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:samp contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:samp text + ], + }; + testElm("samp_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:section without an accessible name + + obj = { + role: ROLE_SECTION, + absentAttributes: { "xml-roles": "region" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("section", obj); + + // HTML:section with an accessible name + + obj = { + role: ROLE_REGION, + attributes: { "xml-roles": "region" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("named_section", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:small contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "font-size": "10pt" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:small text + ], + }; + testElm("small_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:source + + ok(!isAccessible("source"), "source element is not accessible"); + + // //////////////////////////////////////////////////////////////////////// + // HTML:span + + ok(!isAccessible("span"), "span element is not accessible"); + + // //////////////////////////////////////////////////////////////////////// + // html:span with a title attribute, which should make it accessible. + obj = { + role: ROLE_TEXT, + }; + testElm("span_explicit", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:strong contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:strong text + ], + }; + testElm("strong_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:sub contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "text-position": "sub" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:sub text + ], + }; + testElm("sub_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:sup contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "text-position": "super" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:sup text + ], + }; + testElm("sup_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:svg + + obj = { + todo_role: ROLE_GRAPHIC, + }; + testElm("svg", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:textarea + + obj = { + role: ROLE_ENTRY, + extraStates: EXT_STATE_MULTI_LINE | EXT_STATE_EDITABLE, + actions: "activate", + interfaces: [ nsIAccessibleText, nsIAccessibleEditableText ], + }; + testElm("textarea", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:time + + obj = { + role: ROLE_TEXT, + attributes: { "xml-roles": "time", "datetime": "2001-05-15 19:00" }, + interfaces: [ nsIAccessibleText, nsIAccessibleHyperText ], + }; + testElm("time", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:u contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + textAttrs: { + 0: { }, + 6: { "text-underline-style": "solid" }, + }, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:u text + ], + }; + testElm("u_container", obj); + + // //////////////////////////////////////////////////////////////////////// + // HTML:var contained by paragraph + + obj = { + role: ROLE_PARAGRAPH, + children: [ + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:var text + { role: ROLE_TEXT_LEAF }, // plain text + { role: ROLE_TEXT_LEAF }, // HTML:var text + ], + }; + testElm("var_container", obj); + + // //////////////////////////////////////////////////////////////////////// + obj = { // HTML:video + role: ROLE_GROUPING, + }; + testElm("video", obj); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + + </script> +</head> +<body> + + <a target="_blank" + title="Implement figure and figcaption accessibility" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=658272"> + Mozilla Bug 658272 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <a id="a_href" href="www.mozilla.com">mozilla site</a> + <a id="a_nohref">anchor</a> + <table> + <tr> + <td id="td_abbr"><abbr title="World Wide Web">WWW</abbr></td> + </tr> + </table> + <address id="address"> + Mozilla Foundation<br> + 1981 Landings Drive<br> + Building K<br> + Mountain View, CA 94043-0801<br> + USA + </address> + + <map name="atoz_map"> + <area id="area_href" + href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" alt="b" shape="rect"> + <area id="area_nohref" + coords="0,0,13,14" alt="a" shape="rect"> + </map> + <img id="imgmap" width="447" height="15" + usemap="#atoz_map" + src="../letters.gif"> + + <article id="article">A document</article> + <audio id="audio" controls="true"> + <source id="source" src="../bug461281.ogg" type="video/ogg"> + </audio> + + <aside id="aside"> + <p>Some content related to an <article></p> + </aside> + + <p id="b_container">normal<b>bold</b></p> + <p id="bdi_container">User <bdi>إيان</bdi>: 90 points</p> + <p id="bdo_container"><bdo dir="rtl">This text will go right to left.</bdo></p> + + <blockquote id="blockquote" cite="http://developer.mozilla.org"> + <p>This is a quotation taken from the Mozilla Developer Center.</p> + </blockquote> + + <!-- two BRs, both will be present --> + <p id="br_container"><br><br></p> + + <button id="button">button</button> + <form> + <button id="button_default" type="submit">button</button> + </form> + + <canvas id="canvas"></canvas> + + <table id="table"> + <caption id="caption">caption</caption> + <thead> + <tr> + <th>col1</th><th>col2</th> + </tr> + </thead> + <tbody> + <tr> + <th>col1</th><td>cell2</td> + </tr> + </tbody> + <tfoot> + <tr> + <td>cell5</td><td>cell6</td> + </tr> + </tfoot> + </table> + + <p id="cite_container">normal<cite>cite</cite></p> + <p id="code_container">normal<code>code</code></p> + + <table id="colNcolgroup_table"> + <colgroup> + <col> + <col span="2"> + </colgroup> + <tr> + <td>Lime</td> + <td>Lemon</td> + <td>Orange</td> + </tr> + </table> + + <p id="data_container"><data value="8">Eight</data></p> + + <datalist id="datalist"> + <summary id="summary">details</summary> + <option>Paris</option> + <option>San Francisco</option> + </datalist> + <input id="autocomplete_datalist" list="datalist"> + + <dl id="dl"> + <dt>item1</dt><dd>description</dd> + </dl> + + <p id="del_container">normal<del>Removed</del></p> + + <details id="details" open="open"> + <summary>Information</summary> + <p>If your browser supports this element, it should allow you to expand and collapse these details.</p> + </details> + + <details id="details_closed"> + <summary>Information</summary> + <p>If your browser supports this element, it should allow you to expand and collapse these details.</p> + </details> + + <p id="dfn_container"><dfn id="def-internet">The Internet</dfn> is a global + system of interconnected networks that use the Internet Protocol Suite (TCP/IP) + to serve billions of users worldwide.</p> + + <dialog id="dialog" open="true">This is a dialog</dialog> + + <div id="div">div</div> + + <p id="em_container">normal<em>emphasis</em></p> + + <embed id="embed_plugin_windowless" type="application/x-test" + width="300" height="300"></embed> + <embed id="embed_plugin_windowed" type="application/x-test" wmode="window" + width="300" height="300"></embed> + + <embed id="embed_png" type="image/png" src="../moz.png" + width="300" height="300"> + </embed> + <embed id="embed_html" type="text/html" src="../longdesc_src.html" + width="300" height="300"> + </embed> + <embed id="embed_pdf" type="application/pdf" src="../dummy.pdf" + width="300" height="300"> + </embed> + + <fieldset id="fieldset"> + <legend id="legend">legend</legend> + <input /> + </fieldset> + + <!-- Depending on whether or not the image is cached, layout may be able to + optimize away spaces between the figure, img and figcaption tags. As + such, we should keep everything on one line to get consistent results. + --> + <figure id="figure"><img src="../moz.png" alt="An awesome picture"><figcaption id="figcaption">Caption for the awesome picture</figcaption></figure> + + <footer id="footer">Some copyright info</footer> + <article> + <footer id="footer_in_article">Some copyright info</footer> + </article> + <aside> + <footer id="footer_in_aside">Some copyright info</footer> + </aside> + <main> + <footer id="footer_in_main">Some copyright info</footer> + </main> + <nav> + <footer id="footer_in_nav">Some copyright info</footer> + </nav> + <section> + <footer id="footer_in_section">Some copyright info</footer> + </section> + <blockquote> + <footer id="footer_in_blockquote">Some copyright info</footer> + </blockquote> + <details open="true"> + <footer id="footer_in_details">Some copyright info</footer> + </details> + <dialog open="true"> + <footer id="footer_in_dialog">Some copyright info</footer> + </dialog> + <fieldset> + <footer id="footer_in_fieldset">Some copyright info</footer> + </fieldset> + <figure> + <footer id="footer_in_figure">Some copyright info</footer> + </figure> + <table><tr><td> + <footer id="footer_in_td">Some copyright info</footer> + </td></tr></table> + + <form id="form"></form> + <form id="named_form" aria-label="New form"></form> + + <iframe id="frameset_container" + src="data:text/html,<html><frameset><frame src='data:text/html,hi'></frame></frameset></html>"> + </iframe> + + <h1 id="h1">heading1</h1> + <h2 id="h2">heading2</h2> + <h3 id="h3">heading3</h3> + <h4 id="h4">heading4</h4> + <h5 id="h5">heading5</h5> + <h6 id="h6">heading6</h6> + + <header id="header">A logo</header> + <article> + <header id="header_in_article">Not logo</header> + <h1 id="h1_in_article">heading1</h1> + <h2 id="h2_in_article">heading2</h2> + <h3 id="h3_in_article">heading3</h3> + <h4 id="h4_in_article">heading4</h4> + <h5 id="h5_in_article">heading5</h5> + <h6 id="h6_in_article">heading6</h6> + <hgroup> + <h1 id="h1_in_article_in_hgroup">heading1</h1> + <h2 id="h2_in_article_in_hgroup">heading2</h2> + <h3 id="h3_in_article_in_hgroup">heading3</h3> + <h4 id="h4_in_article_in_hgroup">heading4</h4> + <h5 id="h5_in_article_in_hgroup">heading5</h5> + <h6 id="h6_in_article_in_hgroup">heading6</h6> + </hgroup> + </article> + <aside> + <header id="header_in_aside">Not logo</header> + <h1 id="h1_in_aside">heading1</h1> + <h2 id="h2_in_aside">heading2</h2> + <h3 id="h3_in_aside">heading3</h3> + <h4 id="h4_in_aside">heading4</h4> + <h5 id="h5_in_aside">heading5</h5> + <h6 id="h6_in_aside">heading6</h6> + <hgroup> + <h1 id="h1_in_aside_in_hgroup">heading1</h1> + <h2 id="h2_in_aside_in_hgroup">heading2</h2> + <h3 id="h3_in_aside_in_hgroup">heading3</h3> + <h4 id="h4_in_aside_in_hgroup">heading4</h4> + <h5 id="h5_in_aside_in_hgroup">heading5</h5> + <h6 id="h6_in_aside_in_hgroup">heading6</h6> + </hgroup> + </aside> + <main> + <header id="header_in_main">Not logo</header> + </main> + <nav> + <header id="header_in_nav">Not logo</header> + <h1 id="h1_in_nav">heading1</h1> + <h2 id="h2_in_nav">heading2</h2> + <h3 id="h3_in_nav">heading3</h3> + <h4 id="h4_in_nav">heading4</h4> + <h5 id="h5_in_nav">heading5</h5> + <h6 id="h6_in_nav">heading6</h6> + <hgroup> + <h1 id="h1_in_nav_in_hgroup">heading1</h1> + <h2 id="h2_in_nav_in_hgroup">heading2</h2> + <h3 id="h3_in_nav_in_hgroup">heading3</h3> + <h4 id="h4_in_nav_in_hgroup">heading4</h4> + <h5 id="h5_in_nav_in_hgroup">heading5</h5> + <h6 id="h6_in_nav_in_hgroup">heading6</h6> + </hgroup> + </nav> + <section> + <header id="header_in_section">Not logo</header> + <h1 id="h1_in_section">heading1</h1> + <h2 id="h2_in_section">heading2</h2> + <h3 id="h3_in_section">heading3</h3> + <h4 id="h4_in_section">heading4</h4> + <h5 id="h5_in_section">heading5</h5> + <h6 id="h6_in_section">heading6</h6> + <hgroup> + <h1 id="h1_in_section_in_hgroup">heading1</h1> + <h2 id="h2_in_section_in_hgroup">heading2</h2> + <h3 id="h3_in_section_in_hgroup">heading3</h3> + <h4 id="h4_in_section_in_hgroup">heading4</h4> + <h5 id="h5_in_section_in_hgroup">heading5</h5> + <h6 id="h6_in_section_in_hgroup">heading6</h6> + </hgroup> + </section> + <blockquote> + <header id="header_in_blockquote">Not logo</header> + </blockquote> + <details open="true"> + <header id="header_in_details">Not logo</header> + </details> + <dialog open="true"> + <header id="header_in_dialog">Not logo</header> + </dialog> + <fieldset> + <header id="header_in_fieldset">Not logo</header> + </fieldset> + <figure> + <header id="header_in_figure">Not logo</header> + </figure> + <table><tr><td> + <header id="header_in_td">Not logo</header> + </td></tr></table> + + <hr id="hr"> + <p id="i_container">normal<i>italic</i></p> + <img id="img" src="../moz.png"> + + <input id="input_button" type="button" value="Button"> + <input id="input_checkbox" type="checkbox"> + <input id="input_checkbox_true" type="checkbox" checked> + <input id="input_file" type="file"> + <input id="input_image" type="image"> + <input id="input_image_display" type="image" style="display: block"> + <form> + <input id="input_image_default" type="image"> + </form> + <input id="input_submit" type="submit"> + <form> + <input id="input_submit_default" type="submit"> + </form> + <input id="input_number" type="number" value="44"> + <input id="input_text" type="text" value="hi"> + <form> + <label for="input_placeholder_same">Your name</label> + <input id="input_placeholder_same" placeholder="Your name"/> + <label for="input_placeholder_different">First name:</label> + <input id="input_placeholder_different" placeholder="Enter your first name"/> + <input id="input_placeholder_only" placeholder="Date of birth"/> + </form> + <input id="input_search" type="search" value="cats"> + <input id="input_email" type="email" value="me@mozilla.com"> + <input id="input_tel" type="tel" value="111.111.1111"> + <input id="input_url" type="url" value="www.mozilla.com"> + <input id="input_password" type="password" value="44"> + <input id="input_password_readonly" type="password" value="44" readonly> + <input id="input_radio" type="radio"> + <input id="input_radio_true" type="radio" checked> + <input id="input_range" type="range"> + <form> + <input id="input_reset" type="reset"> + </form> + <label>time label + <input id="input_time" type="time" value="23:23"> + </label> + <label>date label + <input id="input_date" type="date" value="2017-11-10"> + </label> + + <p id="ins_container">normal<ins>Inserted</ins></p> + <p id="kbd_container">normal<kbd>cmd</kbd></p> + + <label id="label">label<input id="label_input"></label> + <label id="label_for" for="label_for_input">label</label> + <input id="label_for_input"> + + <ul id="ul"> + <li>item1</li> + </ul> + <ol id="ol"> + <li>item1</li> + </ol> + + <main id="main">main</main> + + <map id="map_imagemap" name="atoz_map"> + <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" alt="b" shape="rect"> + <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a" + coords="0,0,13,14" alt="a" shape="rect"> + </map> + <img id="imgmap" width="447" height="15" + usemap="#atoz_map" + src="../letters.gif"> + + <map id="map" title="Navigation Bar" name="mapgroup"> + <p> + [<a href="#how">Bypass navigation bar</a>] + [<a href="home.html">Home</a>] + </p> + </map> + + <p id="mark_container">normal<mark>highlighted</mark></p> + + <math id="math"> + <mrow> + <mrow> + <msup> + <mi>a</mi> + <mn>2</mn> + </msup> + <mo>+</mo> + <msup> + <mi>b</mi> + <mn>2</mn> + </msup> + </mrow> + <mo>=</mo> + <msup> + <mi>c</mi> + <mn>2</mn> + </msup> + </mrow> + </math> + + <menu id="menu"> + <li>Home</li> + <li>About + <menu> + <li>Our Story</li> + </menu> + </li> + </menu> + + <menu id="menu1"> + <li> + <button>File</button> + <menu> + <li> + <button type="button" onclick="new()">New...</button> + </li> + </menu> + </li> + </menu> + + <meter id="meter" min="0" max="1000" low="300" high="700" value="200">200 Euro</meter> + + <nav id="nav"> + <ul> + <li><a href="#">Home</a></li> + <li><a href="#">About</a></li> + <li><a href="#">Contact</a></li> + </ul> + </nav> + + <object id="object_plugin_windowless" type="application/x-test" + width="300" height="300"> + <param name="foo" value="bar"> + </object> + <object id="object_plugin_windowed" type="application/x-test" wmode="window" + width="300" height="300"></object> + + <object id="object_png" type="image/png" data="../moz.png" + width="300" height="300"> + </object> + <object id="object_html" type="text/html" data="../longdesc_src.html" + width="300" height="300"> + </object> + <object id="object_pdf" type="application/pdf" data="../dummy.pdf" + width="300" height="300"> + </object> + + <select id="select_listbox" size="4"> + <optgroup label="Colors"> + <option>Red</option> + <option>Blue</option> + </optgroup> + <option>Animal</option> + </select> + + <select id="select_listbox_multiselectable" multiple> + <option>Red</option> + <option>Blue</option> + <option>Green</option> + </select> + + <select id="select_combobox"> + <option>Red</option> + <option>Blue</option> + <option>Green</option> + </select> + + <input id="output_input"> + <output id="output" for="output_input"></output> + + <pre id="pre">pre</pre> + + <progress id="progress" min="0" value="21" max="42"></progress> + <progress id="progress_indeterminate"></progress> + + <q id="q" cite="http://en.wikipedia.org/wiki/Kenny_McCormick#Cultural_impact"> + Oh my God, they killed Kenny! + </q> + + <ruby id="ruby"> + 漢 <rp>(</rp><rt>Kan</rt><rp>)</rp> + 字 <rp>(</rp><rt>ji</rt><rp>)</rp> + </ruby> + + <p id="s_container">normal<s>striked</s></p> + <p id="samp_container">normal<samp>sample</samp></p> + <section id="section">section</section> + <section id="named_section" aria-label="foo">named section</section> + <p id="small_container">normal<small>small</small></p> + <span id="span"></span> + <span id="span_explicit" title="explicit"></span> + <p id="strong_container">normal<strong>strong</strong></p> + <p id="sub_container">normal<sub>sub</sub></p> + <p id="sup_container">normal<sup>sup</sup></p> + + <svg id="svg"></svg> + <textarea id="textarea"></textarea> + + <p>The concert took place on <time id="time" datetime="2001-05-15 19:00">May 15</time></p> + <p id="u_container">normal<u>underline</u></p> + <p id="var_container">An equation: <var>x</var> = <var>y</var></p> + + <video id="video" controls="true"> + <source id="source" src="../bug461281.ogg" type="video/ogg"> + </video> + +</video> +</body> +</html> diff --git a/accessible/tests/mochitest/elm/test_MathMLSpec.html b/accessible/tests/mochitest/elm/test_MathMLSpec.html new file mode 100644 index 0000000000..c711acf05f --- /dev/null +++ b/accessible/tests/mochitest/elm/test_MathMLSpec.html @@ -0,0 +1,617 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML a11y spec tests</title> + <link id="link" rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../actions.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // math + + let obj = { + role: ROLE_MATHML_MATH, + }; + testElm("math", obj); + + // //////////////////////////////////////////////////////////////////////// + // mi + + obj = { + role: ROLE_MATHML_IDENTIFIER, + }; + testElm("mi", obj); + + // //////////////////////////////////////////////////////////////////////// + // mn + + obj = { + role: ROLE_MATHML_NUMBER, + }; + testElm("mn", obj); + + // //////////////////////////////////////////////////////////////////////// + // mo + + obj = { + role: ROLE_MATHML_OPERATOR, + attributes: { accent: "true", largeop: "true" }, + }; + testElm("mo", obj); + + obj = { + role: ROLE_MATHML_OPERATOR, + attributes: { fence: "true" }, + }; + testElm("mo_fence", obj); + + obj = { + role: ROLE_MATHML_OPERATOR, + attributes: { separator: "true" }, + }; + testElm("mo_separator", obj); + + // //////////////////////////////////////////////////////////////////////// + // mtext + + obj = { + role: ROLE_MATHML_TEXT, + }; + testElm("mtext", obj); + + // //////////////////////////////////////////////////////////////////////// + // ms + + obj = { + role: ROLE_MATHML_STRING_LITERAL, + }; + testElm("ms", obj); + + // //////////////////////////////////////////////////////////////////////// + // mglyph + + obj = { + role: ROLE_MATHML_GLYPH, + }; + testElm("mglyph", obj); + + // //////////////////////////////////////////////////////////////////////// + // mrow + + obj = { + role: ROLE_MATHML_ROW, + }; + testElm("mrow", obj); + + // //////////////////////////////////////////////////////////////////////// + // mfrac + + obj = { + role: ROLE_MATHML_FRACTION, + attributes: { bevelled: "true", linethickness: "thick" }, + }; + testElm("mfrac", obj); + + // //////////////////////////////////////////////////////////////////////// + // msqrt + + obj = { + role: ROLE_MATHML_SQUARE_ROOT, + }; + testElm("msqrt", obj); + + // //////////////////////////////////////////////////////////////////////// + // mroot + + obj = { + role: ROLE_MATHML_ROOT, + relations: { + RELATION_NODE_PARENT_OF: ["mroot_index", "mroot_base"], + }, + children: [ + { + role: ROLE_MATHML_IDENTIFIER, + relations: { RELATION_NODE_CHILD_OF: "mroot" }, + }, + { + role: ROLE_MATHML_NUMBER, + relations: { RELATION_NODE_CHILD_OF: "mroot" }, + }, + ], + }; + testElm("mroot", obj); + + // //////////////////////////////////////////////////////////////////////// + // mfenced + + obj = { + role: ROLE_MATHML_FENCED, + attributes: { open: "]", close: "[", separators: "." }, + }; + testElm("mfenced", obj); + + // //////////////////////////////////////////////////////////////////////// + // menclose + + obj = { + role: ROLE_MATHML_ENCLOSED, + attributes: { notation: "circle" }, + }; + testElm("menclose", obj); + + // //////////////////////////////////////////////////////////////////////// + // mstyle, mpadded, mphantom + + obj = { + role: ROLE_MATHML_STYLE, + }; + testElm("mstyle", obj); + + ok(!isAccessible("mpadded"), "mpadded should not have accessible"); + ok(!isAccessible("mphantom"), "mphantom should not have accessible"); + + // //////////////////////////////////////////////////////////////////////// + // msub + + obj = { + role: ROLE_MATHML_SUB, + }; + testElm("msub", obj); + + // //////////////////////////////////////////////////////////////////////// + // msup + + obj = { + role: ROLE_MATHML_SUP, + }; + testElm("msup", obj); + + // //////////////////////////////////////////////////////////////////////// + // msubsup + + obj = { + role: ROLE_MATHML_SUB_SUP, + }; + testElm("msubsup", obj); + + // //////////////////////////////////////////////////////////////////////// + // munder + + obj = { + role: ROLE_MATHML_UNDER, + attributes: { accentunder: "true", align: "center" }, + }; + testElm("munder", obj); + + // //////////////////////////////////////////////////////////////////////// + // mover + + obj = { + role: ROLE_MATHML_OVER, + attributes: { accent: "true", align: "center" }, + }; + testElm("mover", obj); + + // //////////////////////////////////////////////////////////////////////// + // munderover + + obj = { + role: ROLE_MATHML_UNDER_OVER, + attributes: { accent: "true", accentunder: "true", align: "center" }, + }; + testElm("munderover", obj); + + // //////////////////////////////////////////////////////////////////////// + // mmultiscripts + + obj = { + role: ROLE_MATHML_MULTISCRIPTS, + }; + testElm("mmultiscripts", obj); + + // //////////////////////////////////////////////////////////////////////// + // mtable + + obj = { + role: ROLE_MATHML_TABLE, + attributes: { align: "center", columnlines: "solid", rowlines: "solid" }, + }; + testElm("mtable", obj); + + // //////////////////////////////////////////////////////////////////////// + // mlabeledtr + + obj = { + role: ROLE_MATHML_LABELED_ROW, + }; + testElm("mlabeledtr", obj); + + // //////////////////////////////////////////////////////////////////////// + // mtr + + obj = { + role: ROLE_MATHML_TABLE_ROW, + }; + testElm("mtr", obj); + + // //////////////////////////////////////////////////////////////////////// + // mtd + + obj = { + role: ROLE_MATHML_CELL, + }; + testElm("mtd", obj); + + // //////////////////////////////////////////////////////////////////////// + // maction + + obj = { + role: ROLE_MATHML_ACTION, + attributes: { actiontype: "toggle", selection: "1" }, + }; + testElm("maction", obj); + + // //////////////////////////////////////////////////////////////////////// + // merror + + obj = { + role: ROLE_MATHML_ERROR, + }; + testElm("merror", obj); + + // //////////////////////////////////////////////////////////////////////// + // semantics, annotation, annotation-xml + ok(!isAccessible("semantics"), "semantics should not have accessible"); + ok(!isAccessible("annotation"), "annotation should not have accessible"); + ok(!isAccessible("annotation-xml"), "annotation-xml should not have accessible"); + + // //////////////////////////////////////////////////////////////////////// + // mstack + + obj = { + role: ROLE_MATHML_STACK, + attributes: { align: "center" }, + }; + testElm("mstack", obj); + + // //////////////////////////////////////////////////////////////////////// + // mlongdiv + + obj = { + role: ROLE_MATHML_LONG_DIVISION, + attributes: { longdivstyle: "stackedrightright" }, + }; + testElm("mlongdiv", obj); + + // //////////////////////////////////////////////////////////////////////// + // msgroup + + obj = { + role: ROLE_MATHML_STACK_GROUP, + attributes: { position: "2", shift: "-1" }, + }; + testElm("msgroup", obj); + + // //////////////////////////////////////////////////////////////////////// + // msrow + + obj = { + role: ROLE_MATHML_STACK_ROW, + attributes: { position: "1" }, + }; + testElm("msrow", obj); + + // //////////////////////////////////////////////////////////////////////// + // mscarries + + obj = { + role: ROLE_MATHML_STACK_CARRIES, + attributes: { location: "nw", position: "1" }, + }; + testElm("mscarries", obj); + + // //////////////////////////////////////////////////////////////////////// + // mscarry + + obj = { + role: ROLE_MATHML_STACK_CARRY, + attributes: { crossout: "updiagonalstrike" }, + }; + testElm("mscarry", obj); + + // //////////////////////////////////////////////////////////////////////// + // msline + + obj = { + role: ROLE_MATHML_STACK_LINE, + attributes: { position: "1" }, + }; + testElm("msline", obj); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + + </script> +</head> +<body> + + <a target="_blank" + title="Implement figure and figcaption accessibility" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=658272"> + Mozilla Bug 658272 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <math id="math"> + <mrow id="mrow"> + <mrow> + <msup id="msup"> + <mi id="mi">a</mi> + <mn id="mn">2</mn> + </msup> + <mo id="mo" accent="true" largeop="true">+</mo> + <msqrt id="msqrt"> + <mn>2</mn> + </msqrt> + </mrow> + <mo>=</mo> + <msub id="msub"> + <mi>c</mi> + <mn>2</mn> + </msub> + </mrow> + <mspace id="mspace" width="1em"/> + <mtext id="mtext">Arbitrary text</mtext> + <mspace width="1em"/> + <ms id="ms">InterpretedStringLiteral</ms> + <mi> + <mglyph id="mglyph" src="../letters.gif" alt="letters"/> + </mi> + <mfrac id="mfrac" bevelled="true" linethickness="thick"> + <mi>x</mi> + <mn>2</mn> + </mfrac> + <mroot id="mroot"> + <mi id="mroot_base">x</mi> + <mn id="mroot_index">5</mn> + </mroot> + <mspace width="1em"/> + <mfenced id="mfenced" close="[" open="]" separators="."> + <mrow> + <mi>x</mi> + <mi>y</mi> + </mrow> + </mfenced> + <mrow> + <mo id="mo_fence" fence="true">[</mo> + <mrow> + X + <mo id="mo_separator" separator="true">,</mo> + Y + </mrow> + <mo fence="true"> closing-fence </mo> + </mrow> + <mspace width="1em"/> + <menclose id="menclose" notation="circle"> + <mi>a</mi> + <mo>+</mo> + <mi>b</mi> + </menclose> + <mstyle id="mstyle" dir="rtl" mathcolor="blue"> + <mpadded id="mpadded" height="100px" width="200px"> + <mi>x</mi> + <mphantom id="mphantom"> + <mo>+</mo> + <mi>y</mi> + </mphantom> + </mpadded> + </mstyle> + + <msubsup id="msubsup"> + <mi>b</mi> + <mn>1</mn> + <mn>2</mn> + </msubsup> + <munder id="munder" accentunder="true" align="center"> + <mrow> + <mi> x </mi> + <mo> + </mo> + <mi> y </mi> + <mo> + </mo> + <mi> z </mi> + </mrow> + <mo> ⏟<!--BOTTOM CURLY BRACKET--> </mo> + </munder> + <mspace width="1em"/> + <mover id="mover" accent="true" align="center"> + <mi> x </mi> + <mo> ^<!--CIRCUMFLEX ACCENT--> </mo> + </mover> + <munderover id="munderover" accentunder="true" accent="true" align="center"> + <mo> ∫<!--INTEGRAL--> </mo> + <mn> 0 </mn> + <mi> ∞<!--INFINITY--> </mi> + </munderover> + <mmultiscripts id="mmultiscripts"> + <mi> R </mi> + <mi> i </mi> + <none/> + <none/> + <mi> j </mi> + <mi> k </mi> + <none/> + <mi> l </mi> + <none/> + </mmultiscripts> + + <mtable id="mtable" align="center" columnlines="solid" rowlines="solid"> + <mlabeledtr id="mlabeledtr"> + <mtd> + <mtext> (2.1) </mtext> + </mtd> + <mtd> + <mrow> + <mi>E</mi> + <mo>=</mo> + <mrow> + <mi>m</mi> + <mo>⁢<!--INVISIBLE TIMES--></mo> + <msup> + <mi>c</mi> + <mn>2</mn> + </msup> + </mrow> + </mrow> + </mtd> + </mlabeledtr> + </mtable> + <mrow> + <mo> ( </mo> + <mtable> + <mtr id="mtr"> + <mtd id="mtd"> <mn>1</mn> </mtd> + <mtd> <mn>0</mn> </mtd> + <mtd> <mn>0</mn> </mtd> + </mtr> + <mtr> + <mtd> <mn>0</mn> </mtd> + <mtd> <mn>1</mn> </mtd> + <mtd> <mn>0</mn> </mtd> + </mtr> + <mtr> + <mtd> <mn>0</mn> </mtd> + <mtd> <mn>0</mn> </mtd> + <mtd> <mn>1</mn> </mtd> + </mtr> + </mtable> + <mo> ) </mo> + </mrow> + + <maction id="maction" actiontype="toggle" selection="1"> + <mfrac> + <mn>6</mn> + <mn>8</mn> + </mfrac> + <mfrac> + <mrow> + <mn>3</mn> + <mo>⋅</mo> + <mn>2</mn> + </mrow> + <mrow> + <mn>4</mn> + <mo>⋅</mo> + <mn>2</mn> + </mrow> + </mfrac> + <mfrac> + <mn>3</mn> + <mn>4</mn> + </mfrac> + </maction> + + <merror id="merror"> + <mrow> + <mtext>Division by zero: </mtext> + <mfrac> + <mn>1</mn> + <mn>0</mn> + </mfrac> + </mrow> + </merror> + + <semantics id="semantics"> + <!-- Presentation MathML --> + <mrow> + <msup> + <mi>x</mi> + <mn>2</mn> + </msup> + <mo>+</mo> + <mi>y</mi> + </mrow> + <!-- Content MathML --> + <annotation-xml id="annotation-xml" encoding="MathML-Content"> + <apply> + <plus/> + <apply> + <power/> + <ci>x</ci> + <cn type="integer">2</cn> + </apply> + <ci>y</ci> + </apply> + </annotation-xml> + <!-- annotate TeX --> + <annotation id="annotation" encoding="application/x-tex"> + x^{2} + y + </annotation> + </semantics> + + <mstack id="mstack" align="center"> + <mscarries id="mscarries" location="nw" position="1"> + <none/> + <mscarry id="mscarry" crossout="updiagonalstrike"> + <mn>1</mn> + </mscarry> + <mscarry location="w"> + <mn>1</mn> + </mscarry> + </mscarries> + <mn>523</mn> + <msrow id="msrow" position="1"> + <mo>-</mo> + <none/> + <mn>15</mn> + </msrow> + <msline id="msline" position="1"/> + <mn>508</mn> + </mstack> + <mspace width="1em"/> + <mlongdiv id="mlongdiv" longdivstyle="stackedrightright"> + <mn>5</mn> + <mn>1</mn> + <mn>5</mn> + </mlongdiv> + + <mstack> + <msgroup id="msgroup" position="2" shift="-1"> + <mn>123</mn> + <msrow><mo>×<!--MULTIPLICATION SIGN--></mo><mn>321</mn></msrow> + </msgroup> + <msline/> + <msgroup shift="1"> + <mn>123</mn> + <mn>246</mn> + <mn>369</mn> + </msgroup> + <msline/> + </mstack> + </math> + +</body> +</html> diff --git a/accessible/tests/mochitest/elm/test_canvas.html b/accessible/tests/mochitest/elm/test_canvas.html new file mode 100644 index 0000000000..65d6d4bca4 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_canvas.html @@ -0,0 +1,55 @@ +<!DOCTYPE html> +<html> +<head> + <title>Accessible boundaries for hit regions</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + + <script type="application/javascript"> + var kX = 10, kY = 10, kWidth = 150, kHeight = 100; + function doTest() { + var canv = document.getElementById("c"); + var context = canv.getContext("2d"); + var element = document.getElementById("showA"); + context.beginPath(); + context.rect(kX, kY, kWidth, kHeight); + context.addHitRegion({control: element}); + + var input = getAccessible("showA"); + var [cnvX, cnvY, /* cnvWidth */, /* cnvHeight */] = getBoundsForDOMElm(canv); + var [accX, accY, accWidth, accHeight] = getBounds(input); + + var [x, y, w, h] = CSSToDevicePixels(window, kX, kY, kWidth, kHeight); + is(accX, cnvX + x, "wrong accX"); + is(accY, cnvY + y, "wrong accY"); + is(accWidth, w, "wrong accWidth"); + is(accHeight, h, "wrong accHeight"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(function() { + SpecialPowers.pushPrefEnv({"set": [["canvas.hitregions.enabled", true]]}, doTest); + }); + + </script> +</head> +<body> + + <canvas id="c"> + <input id="showA" type="checkbox"><label for="showA"> Show As </label> + </canvas> + +</body> +</html> diff --git a/accessible/tests/mochitest/elm/test_figure.html b/accessible/tests/mochitest/elm/test_figure.html new file mode 100644 index 0000000000..82ac961e36 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_figure.html @@ -0,0 +1,60 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML5 figure/figcaption tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <script type="application/javascript"> + + function doTest() { + testRole("figure", ROLE_FIGURE); + testRole("figcaption", ROLE_CAPTION); + + todo(false, "figure name gets extra whitespace in the end!"); + testName("figure", "figure caption"); + testName("figcaption", null); + + testRelation("figure", RELATION_LABELLED_BY, "figcaption"); + testRelation("figcaption", RELATION_LABEL_FOR, "figure"); + + testAttrs("figure", {"xml-roles": "figure"}, true); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Implement figure and figcaption accessibility" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=658272"> + Mozilla Bug 658272 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <figure id="figure"> + <figcaption id="figcaption">figure caption</figcaption> + </figure> + +</body> +</html> diff --git a/accessible/tests/mochitest/elm/test_listbox.xhtml b/accessible/tests/mochitest/elm/test_listbox.xhtml new file mode 100644 index 0000000000..2315959e3a --- /dev/null +++ b/accessible/tests/mochitest/elm/test_listbox.xhtml @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL listbox element test."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + <![CDATA[ + function doTest() + { + var id = ""; + var acc = null; + + ////////////////////////////////////////////////////////////////////////// + // Simple listbox. There is no nsIAccessibleTable interface. + + id = "listbox1"; + acc = getAccessible(id); + + // query nsIAccessibleTable + try { + acc.QueryInterface(nsIAccessibleTable); + ok(false, + id + " shouldn't implement nsIAccessibleTable interface."); + } catch(e) { + ok(true, id + " doesn't implement nsIAccessibleTable interface."); + } + + // role + testRole(id, ROLE_LISTBOX); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=418371" + title="implement the rest of methods of nsIAccessibleTable on xul:listbox"> + Mozilla Bug 418371 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <label control="listbox1" value="listbox: "/> + <richlistbox id="listbox1"> + <richlistitem id="item1"><label value="item1"/></richlistitem> + <richlistitem id="item1"><label value="item2"/></richlistitem> + </richlistbox> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/elm/test_nsApplicationAcc.html b/accessible/tests/mochitest/elm/test_nsApplicationAcc.html new file mode 100644 index 0000000000..2e7aabf882 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_nsApplicationAcc.html @@ -0,0 +1,67 @@ +<html> + +<head> + <title>application accessible name</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + var accessible = getApplicationAccessible(); + if (!accessible) { + SimpleTest.finish(); + return; + } + + var brandBundle = + Services.strings.createBundle("chrome://branding/locale/brand.properties"); + + // nsIAccessible::name + var applicationName = ""; + if (LINUX || SOLARIS) { + applicationName = Services.appinfo.name; + } else { + try { + applicationName = brandBundle.GetStringFromName("brandShortName"); + } catch (e) { + } + + if (applicationName == "") + applicationName = "Gecko based application"; + } + is(accessible.name, applicationName, "wrong application accessible name"); + + // nsIAccessibleApplication + is(accessible.appName, Services.appinfo.name, "Wrong application name"); + is(accessible.appVersion, Services.appinfo.version, "Wrong application version"); + is(accessible.platformName, "Gecko", "Wrong platform name"); + is(accessible.platformVersion, Services.appinfo.platformVersion, + "Wrong platform version"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + </head> + <body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=456121" + title="nsApplicationAccessible::GetName does not return a default value when brand.properties does not exist"> + Mozilla Bug 454211 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + </body> +</html> diff --git a/accessible/tests/mochitest/elm/test_shadowroot.html b/accessible/tests/mochitest/elm/test_shadowroot.html new file mode 100644 index 0000000000..bc221090b4 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_shadowroot.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> +<head> + <title>ShadowRoot tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +</head> +<body> + + <a target="_blank" + title="Ensure accessible objects are created for shadow root" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1026125"> + Mozilla Bug 1026125 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <script> + SimpleTest.waitForExplicitFinish(); + + window.onload = () => { + var iframe = document.createElement("iframe"); + iframe.src = "test_shadowroot_subframe.html"; + document.body.appendChild(iframe); + }; + + </script> + +</body> +</html> diff --git a/accessible/tests/mochitest/elm/test_shadowroot_subframe.html b/accessible/tests/mochitest/elm/test_shadowroot_subframe.html new file mode 100644 index 0000000000..fe158f2cf4 --- /dev/null +++ b/accessible/tests/mochitest/elm/test_shadowroot_subframe.html @@ -0,0 +1,68 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>ShadowRoot tests</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="../common.js"></script> + <script type="application/javascript" src="../role.js"></script> + + <script type="application/javascript"> + let SimpleTest = window.parent.SimpleTest; + let ok = window.parent.ok; + let is = window.parent.is; + + function doTest() { + testElm("component", { + role: ROLE_GROUPING, + children: [ + { + role: ROLE_PUSHBUTTON, + }, + { + role: ROLE_LINK, + }, + ], + }); + + // Shadow root boundary between table and row + testElm("table", { + role: ROLE_TABLE, + children: [ + { + role: ROLE_ROW, + }, + ], + }); + + SimpleTest.finish(); + } + + addA11yLoadEvent(doTest); + </script> + +</head> +<body> + <div role="group" id="component"></div> + <div id="table" role="table" style="display: table;"></div> + + <script> + var component = document.getElementById("component"); + var shadow = component.attachShadow({mode: "open"}); + + var button = document.createElement("button"); + button.append("Hello"); + + var a = document.createElement("a"); + a.setAttribute("href", "#"); + a.append(" World"); + + shadow.appendChild(button); + shadow.appendChild(a); + + var table = document.getElementById("table"); + shadow = table.attachShadow({mode: "open"}); + shadow.innerHTML = "<div style='display: table-row'>" + + "<div style='display: table-cell'>hi</div>" + + "</div>"; + </script> +</body> diff --git a/accessible/tests/mochitest/events.js b/accessible/tests/mochitest/events.js new file mode 100644 index 0000000000..2d5e3d9e38 --- /dev/null +++ b/accessible/tests/mochitest/events.js @@ -0,0 +1,2631 @@ +/* import-globals-from common.js */ +/* import-globals-from states.js */ +/* import-globals-from text.js */ + +// XXX Bug 1425371 - enable no-redeclare and fix the issues with the tests. +/* eslint-disable no-redeclare */ + +// ////////////////////////////////////////////////////////////////////////////// +// Constants + +const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; +const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT; +const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE; +const EVENT_DOCUMENT_LOAD_COMPLETE = + nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; +const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD; +const EVENT_DOCUMENT_LOAD_STOPPED = + nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_STOPPED; +const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE; +const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; +const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; +const EVENT_MENU_START = nsIAccessibleEvent.EVENT_MENU_START; +const EVENT_MENU_END = nsIAccessibleEvent.EVENT_MENU_END; +const EVENT_MENUPOPUP_START = nsIAccessibleEvent.EVENT_MENUPOPUP_START; +const EVENT_MENUPOPUP_END = nsIAccessibleEvent.EVENT_MENUPOPUP_END; +const EVENT_OBJECT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED; +const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; +const EVENT_SCROLLING_START = nsIAccessibleEvent.EVENT_SCROLLING_START; +const EVENT_SELECTION = nsIAccessibleEvent.EVENT_SELECTION; +const EVENT_SELECTION_ADD = nsIAccessibleEvent.EVENT_SELECTION_ADD; +const EVENT_SELECTION_REMOVE = nsIAccessibleEvent.EVENT_SELECTION_REMOVE; +const EVENT_SELECTION_WITHIN = nsIAccessibleEvent.EVENT_SELECTION_WITHIN; +const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; +const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; +const EVENT_TEXT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; +const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; +const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; +const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; +const EVENT_TEXT_SELECTION_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; +const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE; +const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE; +const EVENT_VIRTUALCURSOR_CHANGED = + nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED; + +const kNotFromUserInput = 0; +const kFromUserInput = 1; + +// ////////////////////////////////////////////////////////////////////////////// +// General + +var { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); + +/** + * Set up this variable to dump events into DOM. + */ +var gA11yEventDumpID = ""; + +/** + * Set up this variable to dump event processing into console. + */ +var gA11yEventDumpToConsole = false; + +/** + * Set up this variable to dump event processing into error console. + */ +var gA11yEventDumpToAppConsole = false; + +/** + * Semicolon separated set of logging features. + */ +var gA11yEventDumpFeature = ""; + +/** + * Function to detect HTML elements when given a node. + */ +function isHTMLElement(aNode) { + return ( + aNode.nodeType == aNode.ELEMENT_NODE && + aNode.namespaceURI == "http://www.w3.org/1999/xhtml" + ); +} + +function isXULElement(aNode) { + return ( + aNode.nodeType == aNode.ELEMENT_NODE && + aNode.namespaceURI == + "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + ); +} + +/** + * Executes the function when requested event is handled. + * + * @param aEventType [in] event type + * @param aTarget [in] event target + * @param aFunc [in] function to call when event is handled + * @param aContext [in, optional] object in which context the function is + * called + * @param aArg1 [in, optional] argument passed into the function + * @param aArg2 [in, optional] argument passed into the function + */ +function waitForEvent( + aEventType, + aTargetOrFunc, + aFunc, + aContext, + aArg1, + aArg2 +) { + var handler = { + handleEvent: function handleEvent(aEvent) { + var target = aTargetOrFunc; + if (typeof aTargetOrFunc == "function") { + target = aTargetOrFunc.call(); + } + + if (target) { + if (target instanceof nsIAccessible && target != aEvent.accessible) { + return; + } + + if (Node.isInstance(target) && target != aEvent.DOMNode) { + return; + } + } + + unregisterA11yEventListener(aEventType, this); + + window.setTimeout(function() { + aFunc.call(aContext, aArg1, aArg2); + }, 0); + }, + }; + + registerA11yEventListener(aEventType, handler); +} + +/** + * Generate mouse move over image map what creates image map accessible (async). + * See waitForImageMap() function. + */ +function waveOverImageMap(aImageMapID) { + var imageMapNode = getNode(aImageMapID); + synthesizeMouse( + imageMapNode, + 10, + 10, + { type: "mousemove" }, + imageMapNode.ownerGlobal + ); +} + +/** + * Call the given function when the tree of the given image map is built. + */ +function waitForImageMap(aImageMapID, aTestFunc) { + waveOverImageMap(aImageMapID); + + var imageMapAcc = getAccessible(aImageMapID); + if (imageMapAcc.firstChild) { + aTestFunc(); + return; + } + + waitForEvent(EVENT_REORDER, imageMapAcc, aTestFunc); +} + +/** + * Register accessibility event listener. + * + * @param aEventType the accessible event type (see nsIAccessibleEvent for + * available constants). + * @param aEventHandler event listener object, when accessible event of the + * given type is handled then 'handleEvent' method of + * this object is invoked with nsIAccessibleEvent object + * as the first argument. + */ +function registerA11yEventListener(aEventType, aEventHandler) { + listenA11yEvents(true); + addA11yEventListener(aEventType, aEventHandler); +} + +/** + * Unregister accessibility event listener. Must be called for every registered + * event listener (see registerA11yEventListener() function) when the listener + * is not needed. + */ +function unregisterA11yEventListener(aEventType, aEventHandler) { + removeA11yEventListener(aEventType, aEventHandler); + listenA11yEvents(false); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue + +/** + * Return value of invoke method of invoker object. Indicates invoker was unable + * to prepare action. + */ +const INVOKER_ACTION_FAILED = 1; + +/** + * Return value of eventQueue.onFinish. Indicates eventQueue should not finish + * tests. + */ +const DO_NOT_FINISH_TEST = 1; + +/** + * Creates event queue for the given event type. The queue consists of invoker + * objects, each of them generates the event of the event type. When queue is + * started then every invoker object is asked to generate event after timeout. + * When event is caught then current invoker object is asked to check whether + * event was handled correctly. + * + * Invoker interface is: + * + * var invoker = { + * // Generates accessible event or event sequence. If returns + * // INVOKER_ACTION_FAILED constant then stop tests. + * invoke: function(){}, + * + * // [optional] Invoker's check of handled event for correctness. + * check: function(aEvent){}, + * + * // [optional] Invoker's check before the next invoker is proceeded. + * finalCheck: function(aEvent){}, + * + * // [optional] Is called when event of any registered type is handled. + * debugCheck: function(aEvent){}, + * + * // [ignored if 'eventSeq' is defined] DOM node event is generated for + * // (used in the case when invoker expects single event). + * DOMNode getter: function() {}, + * + * // [optional] if true then event sequences are ignored (no failure if + * // sequences are empty). Use you need to invoke an action, do some check + * // after timeout and proceed a next invoker. + * noEventsOnAction getter: function() {}, + * + * // Array of checker objects defining expected events on invoker's action. + * // + * // Checker object interface: + * // + * // var checker = { + * // * DOM or a11y event type. * + * // type getter: function() {}, + * // + * // * DOM node or accessible. * + * // target getter: function() {}, + * // + * // * DOM event phase (false - bubbling). * + * // phase getter: function() {}, + * // + * // * Callback, called to match handled event. * + * // match : function(aEvent) {}, + * // + * // * Callback, called when event is handled + * // check: function(aEvent) {}, + * // + * // * Checker ID * + * // getID: function() {}, + * // + * // * Event that don't have predefined order relative other events. * + * // async getter: function() {}, + * // + * // * Event that is not expected. * + * // unexpected getter: function() {}, + * // + * // * No other event of the same type is not allowed. * + * // unique getter: function() {} + * // }; + * eventSeq getter() {}, + * + * // Array of checker objects defining unexpected events on invoker's + * // action. + * unexpectedEventSeq getter() {}, + * + * // The ID of invoker. + * getID: function(){} // returns invoker ID + * }; + * + * // Used to add a possible scenario of expected/unexpected events on + * // invoker's action. + * defineScenario(aInvokerObj, aEventSeq, aUnexpectedEventSeq) + * + * + * @param aEventType [in, optional] the default event type (isn't used if + * invoker defines eventSeq property). + */ +function eventQueue(aEventType) { + // public + + /** + * Add invoker object into queue. + */ + this.push = function eventQueue_push(aEventInvoker) { + this.mInvokers.push(aEventInvoker); + }; + + /** + * Start the queue processing. + */ + this.invoke = function eventQueue_invoke() { + listenA11yEvents(true); + + // XXX: Intermittent test_events_caretmove.html fails withouth timeout, + // see bug 474952. + this.processNextInvokerInTimeout(true); + }; + + /** + * This function is called when all events in the queue were handled. + * Override it if you need to be notified of this. + */ + this.onFinish = function eventQueue_finish() {}; + + // private + + /** + * Process next invoker. + */ + // eslint-disable-next-line complexity + this.processNextInvoker = function eventQueue_processNextInvoker() { + // Some scenario was matched, we wait on next invoker processing. + if (this.mNextInvokerStatus == kInvokerCanceled) { + this.setInvokerStatus( + kInvokerNotScheduled, + "scenario was matched, wait for next invoker activation" + ); + return; + } + + this.setInvokerStatus( + kInvokerNotScheduled, + "the next invoker is processed now" + ); + + // Finish processing of the current invoker if any. + var testFailed = false; + + var invoker = this.getInvoker(); + if (invoker) { + if ("finalCheck" in invoker) { + invoker.finalCheck(); + } + + if (this.mScenarios && this.mScenarios.length) { + var matchIdx = -1; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + if (!this.areExpectedEventsLeft(eventSeq)) { + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + if ( + (checker.unexpected && checker.wasCaught) || + (!checker.unexpected && checker.wasCaught != 1) + ) { + break; + } + } + + // Ok, we have matched scenario. Report it was completed ok. In + // case of empty scenario guess it was matched but if later we + // find out that non empty scenario was matched then it will be + // a final match. + if (idx == eventSeq.length) { + if ( + matchIdx != -1 && + eventSeq.length > 0 && + this.mScenarios[matchIdx].length > 0 + ) { + ok( + false, + "We have a matched scenario at index " + + matchIdx + + " already." + ); + } + + if (matchIdx == -1 || eventSeq.length > 0) { + matchIdx = scnIdx; + } + + // Report everything is ok. + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + var typeStr = eventQueue.getEventTypeAsString(checker); + var msg = + "Test with ID = '" + this.getEventID(checker) + "' succeed. "; + + if (checker.unexpected) { + ok(true, msg + `There's no unexpected '${typeStr}' event.`); + } else if (checker.todo) { + todo(false, `Todo event '${typeStr}' was caught`); + } else { + ok(true, `${msg} Event '${typeStr}' was handled.`); + } + } + } + } + } + + // We don't have completely matched scenario. Report each failure/success + // for every scenario. + if (matchIdx == -1) { + testFailed = true; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + var typeStr = eventQueue.getEventTypeAsString(checker); + var msg = + "Scenario #" + + scnIdx + + " of test with ID = '" + + this.getEventID(checker) + + "' failed. "; + + if (checker.wasCaught > 1) { + ok(false, msg + "Dupe " + typeStr + " event."); + } + + if (checker.unexpected) { + if (checker.wasCaught) { + ok(false, msg + "There's unexpected " + typeStr + " event."); + } + } else if (!checker.wasCaught) { + var rf = checker.todo ? todo : ok; + rf(false, `${msg} '${typeStr} event is missed.`); + } + } + } + } + } + } + + this.clearEventHandler(); + + // Check if need to stop the test. + if (testFailed || this.mIndex == this.mInvokers.length - 1) { + listenA11yEvents(false); + + var res = this.onFinish(); + if (res != DO_NOT_FINISH_TEST) { + SimpleTest.executeSoon(SimpleTest.finish); + } + + return; + } + + // Start processing of next invoker. + invoker = this.getNextInvoker(); + + // Set up event listeners. Process a next invoker if no events were added. + if (!this.setEventHandler(invoker)) { + this.processNextInvoker(); + return; + } + + if (gLogger.isEnabled()) { + gLogger.logToConsole("Event queue: \n invoke: " + invoker.getID()); + gLogger.logToDOM("EQ: invoke: " + invoker.getID(), true); + } + + var infoText = "Invoke the '" + invoker.getID() + "' test { "; + var scnCount = this.mScenarios ? this.mScenarios.length : 0; + for (var scnIdx = 0; scnIdx < scnCount; scnIdx++) { + infoText += "scenario #" + scnIdx + ": "; + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + infoText += eventSeq[idx].unexpected + ? "un" + : "" + + "expected '" + + eventQueue.getEventTypeAsString(eventSeq[idx]) + + "' event; "; + } + } + infoText += " }"; + info(infoText); + + if (invoker.invoke() == INVOKER_ACTION_FAILED) { + // Invoker failed to prepare action, fail and finish tests. + this.processNextInvoker(); + return; + } + + if (this.hasUnexpectedEventsScenario()) { + this.processNextInvokerInTimeout(true); + } + }; + + this.processNextInvokerInTimeout = function eventQueue_processNextInvokerInTimeout( + aUncondProcess + ) { + this.setInvokerStatus(kInvokerPending, "Process next invoker in timeout"); + + // No need to wait extra timeout when a) we know we don't need to do that + // and b) there's no any single unexpected event. + if (!aUncondProcess && this.areAllEventsExpected()) { + // We need delay to avoid events coalesce from different invokers. + var queue = this; + SimpleTest.executeSoon(function() { + queue.processNextInvoker(); + }); + return; + } + + // Check in timeout invoker didn't fire registered events. + window.setTimeout( + function(aQueue) { + aQueue.processNextInvoker(); + }, + 300, + this + ); + }; + + /** + * Handle events for the current invoker. + */ + // eslint-disable-next-line complexity + this.handleEvent = function eventQueue_handleEvent(aEvent) { + var invoker = this.getInvoker(); + if (!invoker) { + // skip events before test was started + return; + } + + if (!this.mScenarios) { + // Bad invoker object, error will be reported before processing of next + // invoker in the queue. + this.processNextInvoker(); + return; + } + + if ("debugCheck" in invoker) { + invoker.debugCheck(aEvent); + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var checker = eventSeq[idx]; + + // Search through handled expected events to report error if one of them + // is handled for a second time. + if ( + !checker.unexpected && + checker.wasCaught > 0 && + eventQueue.isSameEvent(checker, aEvent) + ) { + checker.wasCaught++; + continue; + } + + // Search through unexpected events, any match results in error report + // after this invoker processing (in case of matched scenario only). + if (checker.unexpected && eventQueue.compareEvents(checker, aEvent)) { + checker.wasCaught++; + continue; + } + + // Report an error if we hanlded not expected event of unique type + // (i.e. event types are matched, targets differs). + if ( + !checker.unexpected && + checker.unique && + eventQueue.compareEventTypes(checker, aEvent) + ) { + var isExpected = false; + for (var jdx = 0; jdx < eventSeq.length; jdx++) { + isExpected = eventQueue.compareEvents(eventSeq[jdx], aEvent); + if (isExpected) { + break; + } + } + + if (!isExpected) { + ok( + false, + "Unique type " + + eventQueue.getEventTypeAsString(checker) + + " event was handled." + ); + } + } + } + } + + var hasMatchedCheckers = false; + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + + // Check if handled event matches expected sync event. + var nextChecker = this.getNextExpectedEvent(eventSeq); + if (nextChecker) { + if (eventQueue.compareEvents(nextChecker, aEvent)) { + this.processMatchedChecker(aEvent, nextChecker, scnIdx, eventSeq.idx); + hasMatchedCheckers = true; + continue; + } + } + + // Check if handled event matches any expected async events. + var haveUnmatchedAsync = false; + for (idx = 0; idx < eventSeq.length; idx++) { + if (eventSeq[idx] instanceof orderChecker && haveUnmatchedAsync) { + break; + } + + if (!eventSeq[idx].wasCaught) { + haveUnmatchedAsync = true; + } + + if (!eventSeq[idx].unexpected && eventSeq[idx].async) { + if (eventQueue.compareEvents(eventSeq[idx], aEvent)) { + this.processMatchedChecker(aEvent, eventSeq[idx], scnIdx, idx); + hasMatchedCheckers = true; + break; + } + } + } + } + + if (hasMatchedCheckers) { + var invoker = this.getInvoker(); + if ("check" in invoker) { + invoker.check(aEvent); + } + } + + for (idx = 0; idx < eventSeq.length; idx++) { + if (!eventSeq[idx].wasCaught) { + if (eventSeq[idx] instanceof orderChecker) { + eventSeq[idx].wasCaught++; + } else { + break; + } + } + } + + // If we don't have more events to wait then schedule next invoker. + if (this.hasMatchedScenario()) { + if (this.mNextInvokerStatus == kInvokerNotScheduled) { + this.processNextInvokerInTimeout(); + } else if (this.mNextInvokerStatus == kInvokerCanceled) { + this.setInvokerStatus( + kInvokerPending, + "Full match. Void the cancelation of next invoker processing" + ); + } + return; + } + + // If we have scheduled a next invoker then cancel in case of match. + if (this.mNextInvokerStatus == kInvokerPending && hasMatchedCheckers) { + this.setInvokerStatus( + kInvokerCanceled, + "Cancel the scheduled invoker in case of match" + ); + } + }; + + // Helpers + this.processMatchedChecker = function eventQueue_function( + aEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx + ) { + aMatchedChecker.wasCaught++; + + if ("check" in aMatchedChecker) { + aMatchedChecker.check(aEvent); + } + + eventQueue.logEvent( + aEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx, + this.areExpectedEventsLeft(), + this.mNextInvokerStatus + ); + }; + + this.getNextExpectedEvent = function eventQueue_getNextExpectedEvent( + aEventSeq + ) { + if (!("idx" in aEventSeq)) { + aEventSeq.idx = 0; + } + + while ( + aEventSeq.idx < aEventSeq.length && + (aEventSeq[aEventSeq.idx].unexpected || + aEventSeq[aEventSeq.idx].todo || + aEventSeq[aEventSeq.idx].async || + aEventSeq[aEventSeq.idx] instanceof orderChecker || + aEventSeq[aEventSeq.idx].wasCaught > 0) + ) { + aEventSeq.idx++; + } + + return aEventSeq.idx != aEventSeq.length ? aEventSeq[aEventSeq.idx] : null; + }; + + this.areExpectedEventsLeft = function eventQueue_areExpectedEventsLeft( + aScenario + ) { + function scenarioHasUnhandledExpectedEvent(aEventSeq) { + // Check if we have unhandled async (can be anywhere in the sequance) or + // sync expcected events yet. + for (var idx = 0; idx < aEventSeq.length; idx++) { + if ( + !aEventSeq[idx].unexpected && + !aEventSeq[idx].todo && + !aEventSeq[idx].wasCaught && + !(aEventSeq[idx] instanceof orderChecker) + ) { + return true; + } + } + + return false; + } + + if (aScenario) { + return scenarioHasUnhandledExpectedEvent(aScenario); + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + if (scenarioHasUnhandledExpectedEvent(eventSeq)) { + return true; + } + } + return false; + }; + + this.areAllEventsExpected = function eventQueue_areAllEventsExpected() { + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + if (eventSeq[idx].unexpected || eventSeq[idx].todo) { + return false; + } + } + } + + return true; + }; + + this.isUnexpectedEventScenario = function eventQueue_isUnexpectedEventsScenario( + aScenario + ) { + for (var idx = 0; idx < aScenario.length; idx++) { + if (!aScenario[idx].unexpected && !aScenario[idx].todo) { + break; + } + } + + return idx == aScenario.length; + }; + + this.hasUnexpectedEventsScenario = function eventQueue_hasUnexpectedEventsScenario() { + if (this.getInvoker().noEventsOnAction) { + return true; + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + if (this.isUnexpectedEventScenario(this.mScenarios[scnIdx])) { + return true; + } + } + + return false; + }; + + this.hasMatchedScenario = function eventQueue_hasMatchedScenario() { + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var scn = this.mScenarios[scnIdx]; + if ( + !this.isUnexpectedEventScenario(scn) && + !this.areExpectedEventsLeft(scn) + ) { + return true; + } + } + return false; + }; + + this.getInvoker = function eventQueue_getInvoker() { + return this.mInvokers[this.mIndex]; + }; + + this.getNextInvoker = function eventQueue_getNextInvoker() { + return this.mInvokers[++this.mIndex]; + }; + + this.setEventHandler = function eventQueue_setEventHandler(aInvoker) { + if (!("scenarios" in aInvoker) || aInvoker.scenarios.length == 0) { + var eventSeq = aInvoker.eventSeq; + var unexpectedEventSeq = aInvoker.unexpectedEventSeq; + if (!eventSeq && !unexpectedEventSeq && this.mDefEventType) { + eventSeq = [new invokerChecker(this.mDefEventType, aInvoker.DOMNode)]; + } + + if (eventSeq || unexpectedEventSeq) { + defineScenario(aInvoker, eventSeq, unexpectedEventSeq); + } + } + + if (aInvoker.noEventsOnAction) { + return true; + } + + this.mScenarios = aInvoker.scenarios; + if (!this.mScenarios || !this.mScenarios.length) { + ok(false, "Broken invoker '" + aInvoker.getID() + "'"); + return false; + } + + // Register event listeners. + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + + if (gLogger.isEnabled()) { + var msg = + "scenario #" + + scnIdx + + ", registered events number: " + + eventSeq.length; + gLogger.logToConsole(msg); + gLogger.logToDOM(msg, true); + } + + // Do not warn about empty event sequances when more than one scenario + // was registered. + if (this.mScenarios.length == 1 && eventSeq.length == 0) { + ok( + false, + "Broken scenario #" + + scnIdx + + " of invoker '" + + aInvoker.getID() + + "'. No registered events" + ); + return false; + } + + for (var idx = 0; idx < eventSeq.length; idx++) { + eventSeq[idx].wasCaught = 0; + } + + for (var idx = 0; idx < eventSeq.length; idx++) { + if (gLogger.isEnabled()) { + var msg = "registered"; + if (eventSeq[idx].unexpected) { + msg += " unexpected"; + } + if (eventSeq[idx].async) { + msg += " async"; + } + + msg += + ": event type: " + + eventQueue.getEventTypeAsString(eventSeq[idx]) + + ", target: " + + eventQueue.getEventTargetDescr(eventSeq[idx], true); + + gLogger.logToConsole(msg); + gLogger.logToDOM(msg, true); + } + + var eventType = eventSeq[idx].type; + if (typeof eventType == "string") { + // DOM event + var target = eventQueue.getEventTarget(eventSeq[idx]); + if (!target) { + ok(false, "no target for DOM event!"); + return false; + } + var phase = eventQueue.getEventPhase(eventSeq[idx]); + target.addEventListener(eventType, this, phase); + } else { + // A11y event + addA11yEventListener(eventType, this); + } + } + } + + return true; + }; + + this.clearEventHandler = function eventQueue_clearEventHandler() { + if (!this.mScenarios) { + return; + } + + for (var scnIdx = 0; scnIdx < this.mScenarios.length; scnIdx++) { + var eventSeq = this.mScenarios[scnIdx]; + for (var idx = 0; idx < eventSeq.length; idx++) { + var eventType = eventSeq[idx].type; + if (typeof eventType == "string") { + // DOM event + var target = eventQueue.getEventTarget(eventSeq[idx]); + var phase = eventQueue.getEventPhase(eventSeq[idx]); + target.removeEventListener(eventType, this, phase); + } else { + // A11y event + removeA11yEventListener(eventType, this); + } + } + } + this.mScenarios = null; + }; + + this.getEventID = function eventQueue_getEventID(aChecker) { + if ("getID" in aChecker) { + return aChecker.getID(); + } + + var invoker = this.getInvoker(); + return invoker.getID(); + }; + + this.setInvokerStatus = function eventQueue_setInvokerStatus( + aStatus, + aLogMsg + ) { + this.mNextInvokerStatus = aStatus; + + // Uncomment it to debug invoker processing logic. + // gLogger.log(eventQueue.invokerStatusToMsg(aStatus, aLogMsg)); + }; + + this.mDefEventType = aEventType; + + this.mInvokers = []; + this.mIndex = -1; + this.mScenarios = null; + + this.mNextInvokerStatus = kInvokerNotScheduled; +} + +// ////////////////////////////////////////////////////////////////////////////// +// eventQueue static members and constants + +const kInvokerNotScheduled = 0; +const kInvokerPending = 1; +const kInvokerCanceled = 2; + +eventQueue.getEventTypeAsString = function eventQueue_getEventTypeAsString( + aEventOrChecker +) { + if (Event.isInstance(aEventOrChecker)) { + return aEventOrChecker.type; + } + + if (aEventOrChecker instanceof nsIAccessibleEvent) { + return eventTypeToString(aEventOrChecker.eventType); + } + + return typeof aEventOrChecker.type == "string" + ? aEventOrChecker.type + : eventTypeToString(aEventOrChecker.type); +}; + +eventQueue.getEventTargetDescr = function eventQueue_getEventTargetDescr( + aEventOrChecker, + aDontForceTarget +) { + if (Event.isInstance(aEventOrChecker)) { + return prettyName(aEventOrChecker.originalTarget); + } + + // XXXbz this block doesn't seem to be reachable... + if (Event.isInstance(aEventOrChecker)) { + return prettyName(aEventOrChecker.accessible); + } + + var descr = aEventOrChecker.targetDescr; + if (descr) { + return descr; + } + + if (aDontForceTarget) { + return "no target description"; + } + + var target = "target" in aEventOrChecker ? aEventOrChecker.target : null; + return prettyName(target); +}; + +eventQueue.getEventPhase = function eventQueue_getEventPhase(aChecker) { + return "phase" in aChecker ? aChecker.phase : true; +}; + +eventQueue.getEventTarget = function eventQueue_getEventTarget(aChecker) { + if ("eventTarget" in aChecker) { + switch (aChecker.eventTarget) { + case "element": + return aChecker.target; + case "document": + default: + return aChecker.target.ownerDocument; + } + } + return aChecker.target.ownerDocument; +}; + +eventQueue.compareEventTypes = function eventQueue_compareEventTypes( + aChecker, + aEvent +) { + var eventType = Event.isInstance(aEvent) ? aEvent.type : aEvent.eventType; + return aChecker.type == eventType; +}; + +eventQueue.compareEvents = function eventQueue_compareEvents(aChecker, aEvent) { + if (!eventQueue.compareEventTypes(aChecker, aEvent)) { + return false; + } + + // If checker provides "match" function then allow the checker to decide + // whether event is matched. + if ("match" in aChecker) { + return aChecker.match(aEvent); + } + + var target1 = aChecker.target; + if (target1 instanceof nsIAccessible) { + var target2 = Event.isInstance(aEvent) + ? getAccessible(aEvent.target) + : aEvent.accessible; + + return target1 == target2; + } + + // If original target isn't suitable then extend interface to support target + // (original target is used in test_elm_media.html). + var target2 = Event.isInstance(aEvent) + ? aEvent.originalTarget + : aEvent.DOMNode; + return target1 == target2; +}; + +eventQueue.isSameEvent = function eventQueue_isSameEvent(aChecker, aEvent) { + // We don't have stored info about handled event other than its type and + // target, thus we should filter text change and state change events since + // they may occur on the same element because of complex changes. + return ( + this.compareEvents(aChecker, aEvent) && + !(aEvent instanceof nsIAccessibleTextChangeEvent) && + !(aEvent instanceof nsIAccessibleStateChangeEvent) + ); +}; + +eventQueue.invokerStatusToMsg = function eventQueue_invokerStatusToMsg( + aInvokerStatus, + aMsg +) { + var msg = "invoker status: "; + switch (aInvokerStatus) { + case kInvokerNotScheduled: + msg += "not scheduled"; + break; + case kInvokerPending: + msg += "pending"; + break; + case kInvokerCanceled: + msg += "canceled"; + break; + } + + if (aMsg) { + msg += " (" + aMsg + ")"; + } + + return msg; +}; + +eventQueue.logEvent = function eventQueue_logEvent( + aOrigEvent, + aMatchedChecker, + aScenarioIdx, + aEventIdx, + aAreExpectedEventsLeft, + aInvokerStatus +) { + // Dump DOM event information. Skip a11y event since it is dumped by + // gA11yEventObserver. + if (Event.isInstance(aOrigEvent)) { + var info = "Event type: " + eventQueue.getEventTypeAsString(aOrigEvent); + info += ". Target: " + eventQueue.getEventTargetDescr(aOrigEvent); + gLogger.logToDOM(info); + } + + var infoMsg = + "unhandled expected events: " + + aAreExpectedEventsLeft + + ", " + + eventQueue.invokerStatusToMsg(aInvokerStatus); + + var currType = eventQueue.getEventTypeAsString(aMatchedChecker); + var currTargetDescr = eventQueue.getEventTargetDescr(aMatchedChecker); + var consoleMsg = + "*****\nScenario " + + aScenarioIdx + + ", event " + + aEventIdx + + " matched: " + + currType + + "\n" + + infoMsg + + "\n*****"; + gLogger.logToConsole(consoleMsg); + + var emphText = "matched "; + var msg = + "EQ event, type: " + + currType + + ", target: " + + currTargetDescr + + ", " + + infoMsg; + gLogger.logToDOM(msg, true, emphText); +}; + +// ////////////////////////////////////////////////////////////////////////////// +// Action sequence + +/** + * Deal with action sequence. Used when you need to execute couple of actions + * each after other one. + */ +function sequence() { + /** + * Append new sequence item. + * + * @param aProcessor [in] object implementing interface + * { + * // execute item action + * process: function() {}, + * // callback, is called when item was processed + * onProcessed: function() {} + * }; + * @param aEventType [in] event type of expected event on item action + * @param aTarget [in] event target of expected event on item action + * @param aItemID [in] identifier of item + */ + this.append = function sequence_append( + aProcessor, + aEventType, + aTarget, + aItemID + ) { + var item = new sequenceItem(aProcessor, aEventType, aTarget, aItemID); + this.items.push(item); + }; + + /** + * Process next sequence item. + */ + this.processNext = function sequence_processNext() { + this.idx++; + if (this.idx >= this.items.length) { + ok(false, "End of sequence: nothing to process!"); + SimpleTest.finish(); + return; + } + + this.items[this.idx].startProcess(); + }; + + this.items = []; + this.idx = -1; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue invokers + +/** + * Defines a scenario of expected/unexpected events. Each invoker can have + * one or more scenarios of events. Only one scenario must be completed. + */ +function defineScenario(aInvoker, aEventSeq, aUnexpectedEventSeq) { + if (!("scenarios" in aInvoker)) { + aInvoker.scenarios = []; + } + + // Create unified event sequence concatenating expected and unexpected + // events. + if (!aEventSeq) { + aEventSeq = []; + } + + for (var idx = 0; idx < aEventSeq.length; idx++) { + aEventSeq[idx].unexpected |= false; + aEventSeq[idx].async |= false; + } + + if (aUnexpectedEventSeq) { + for (var idx = 0; idx < aUnexpectedEventSeq.length; idx++) { + aUnexpectedEventSeq[idx].unexpected = true; + aUnexpectedEventSeq[idx].async = false; + } + + aEventSeq = aEventSeq.concat(aUnexpectedEventSeq); + } + + aInvoker.scenarios.push(aEventSeq); +} + +/** + * Invokers defined below take a checker object (or array of checker objects). + * An invoker listens for default event type registered in event queue object + * until its checker is provided. + * + * Note, checker object or array of checker objects is optional. + */ + +/** + * Click invoker. + */ +function synthClick(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthClick_invoke() { + var targetNode = this.DOMNode; + if (targetNode.nodeType == targetNode.DOCUMENT_NODE) { + targetNode = this.DOMNode.body + ? this.DOMNode.body + : this.DOMNode.documentElement; + } + + // Scroll the node into view, otherwise synth click may fail. + if (isHTMLElement(targetNode)) { + targetNode.scrollIntoView(true); + } else if (isXULElement(targetNode)) { + var targetAcc = getAccessible(targetNode); + targetAcc.scrollTo(SCROLL_TYPE_ANYWHERE); + } + + var x = 1, + y = 1; + if (aArgs && "where" in aArgs && aArgs.where == "right") { + if (isHTMLElement(targetNode)) { + x = targetNode.offsetWidth - 1; + } else if (isXULElement(targetNode)) { + x = targetNode.getBoundingClientRect().width - 1; + } + } + synthesizeMouse(targetNode, x, y, aArgs ? aArgs : {}); + }; + + this.finalCheck = function synthClick_finalCheck() { + // Scroll top window back. + window.top.scrollTo(0, 0); + }; + + this.getID = function synthClick_getID() { + return prettyName(aNodeOrID) + " click"; + }; +} + +/** + * Mouse move invoker. + */ +function synthMouseMove(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); + + this.invoke = function synthMouseMove_invoke() { + synthesizeMouse(this.DOMNode, 1, 1, { type: "mousemove" }); + synthesizeMouse(this.DOMNode, 2, 2, { type: "mousemove" }); + }; + + this.getID = function synthMouseMove_getID() { + return prettyName(aID) + " mouse move"; + }; +} + +/** + * General key press invoker. + */ +function synthKey(aNodeOrID, aKey, aArgs, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthKey_invoke() { + synthesizeKey(this.mKey, this.mArgs, this.mWindow); + }; + + this.getID = function synthKey_getID() { + var key = this.mKey; + switch (this.mKey) { + case "VK_TAB": + key = "tab"; + break; + case "VK_DOWN": + key = "down"; + break; + case "VK_UP": + key = "up"; + break; + case "VK_LEFT": + key = "left"; + break; + case "VK_RIGHT": + key = "right"; + break; + case "VK_HOME": + key = "home"; + break; + case "VK_END": + key = "end"; + break; + case "VK_ESCAPE": + key = "escape"; + break; + case "VK_RETURN": + key = "enter"; + break; + } + if (aArgs) { + if (aArgs.shiftKey) { + key += " shift"; + } + if (aArgs.ctrlKey) { + key += " ctrl"; + } + if (aArgs.altKey) { + key += " alt"; + } + } + return prettyName(aNodeOrID) + " '" + key + " ' key"; + }; + + this.mKey = aKey; + this.mArgs = aArgs ? aArgs : {}; + this.mWindow = aArgs ? aArgs.window : null; +} + +/** + * Tab key invoker. + */ +function synthTab(aNodeOrID, aCheckerOrEventSeq, aWindow) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_TAB", + { shiftKey: false, window: aWindow }, + aCheckerOrEventSeq + ); +} + +/** + * Shift tab key invoker. + */ +function synthShiftTab(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_TAB", + { shiftKey: true }, + aCheckerOrEventSeq + ); +} + +/** + * Escape key invoker. + */ +function synthEscapeKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_ESCAPE", + null, + aCheckerOrEventSeq + ); +} + +/** + * Down arrow key invoker. + */ +function synthDownKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_DOWN", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Up arrow key invoker. + */ +function synthUpKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey(aNodeOrID, "VK_UP", aArgs, aCheckerOrEventSeq); +} + +/** + * Left arrow key invoker. + */ +function synthLeftKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_LEFT", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Right arrow key invoker. + */ +function synthRightKey(aNodeOrID, aCheckerOrEventSeq, aArgs) { + this.__proto__ = new synthKey( + aNodeOrID, + "VK_RIGHT", + aArgs, + aCheckerOrEventSeq + ); +} + +/** + * Home key invoker. + */ +function synthHomeKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aNodeOrID, "VK_HOME", null, aCheckerOrEventSeq); +} + +/** + * End key invoker. + */ +function synthEndKey(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aNodeOrID, "VK_END", null, aCheckerOrEventSeq); +} + +/** + * Enter key invoker + */ +function synthEnterKey(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aID, "VK_RETURN", null, aCheckerOrEventSeq); +} + +/** + * Synth alt + down arrow to open combobox. + */ +function synthOpenComboboxKey(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthDownKey(aID, aCheckerOrEventSeq, { altKey: true }); + + this.getID = function synthOpenComboboxKey_getID() { + return "open combobox (atl + down arrow) " + prettyName(aID); + }; +} + +/** + * Focus invoker. + */ +function synthFocus(aNodeOrID, aCheckerOrEventSeq) { + var checkerOfEventSeq = aCheckerOrEventSeq + ? aCheckerOrEventSeq + : new focusChecker(aNodeOrID); + this.__proto__ = new synthAction(aNodeOrID, checkerOfEventSeq); + + this.invoke = function synthFocus_invoke() { + if (this.DOMNode.editor) { + this.DOMNode.selectionStart = this.DOMNode.selectionEnd = this.DOMNode.value.length; + } + this.DOMNode.focus(); + }; + + this.getID = function synthFocus_getID() { + return prettyName(aNodeOrID) + " focus"; + }; +} + +/** + * Focus invoker. Focus the HTML body of content document of iframe. + */ +function synthFocusOnFrame(aNodeOrID, aCheckerOrEventSeq) { + var frameDoc = getNode(aNodeOrID).contentDocument; + var checkerOrEventSeq = aCheckerOrEventSeq + ? aCheckerOrEventSeq + : new focusChecker(frameDoc); + this.__proto__ = new synthAction(frameDoc, checkerOrEventSeq); + + this.invoke = function synthFocus_invoke() { + this.DOMNode.body.focus(); + }; + + this.getID = function synthFocus_getID() { + return prettyName(aNodeOrID) + " frame document focus"; + }; +} + +/** + * Change the current item when the widget doesn't have a focus. + */ +function changeCurrentItem(aID, aItemID) { + this.eventSeq = [new nofocusChecker()]; + + this.invoke = function changeCurrentItem_invoke() { + var controlNode = getNode(aID); + var itemNode = getNode(aItemID); + + // HTML + if (controlNode.localName == "input") { + if (controlNode.checked) { + this.reportError(); + } + + controlNode.checked = true; + return; + } + + if (controlNode.localName == "select") { + if (controlNode.selectedIndex == itemNode.index) { + this.reportError(); + } + + controlNode.selectedIndex = itemNode.index; + return; + } + + // XUL + if (controlNode.localName == "tree") { + if (controlNode.currentIndex == aItemID) { + this.reportError(); + } + + controlNode.currentIndex = aItemID; + return; + } + + if (controlNode.localName == "menulist") { + if (controlNode.selectedItem == itemNode) { + this.reportError(); + } + + controlNode.selectedItem = itemNode; + return; + } + + if (controlNode.currentItem == itemNode) { + ok( + false, + "Error in test: proposed current item is already current" + + prettyName(aID) + ); + } + + controlNode.currentItem = itemNode; + }; + + this.getID = function changeCurrentItem_getID() { + return "current item change for " + prettyName(aID); + }; + + this.reportError = function changeCurrentItem_reportError() { + ok( + false, + "Error in test: proposed current item '" + + aItemID + + "' is already current" + ); + }; +} + +/** + * Toggle top menu invoker. + */ +function toggleTopMenu(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthKey(aID, "VK_ALT", null, aCheckerOrEventSeq); + + this.getID = function toggleTopMenu_getID() { + return "toggle top menu on " + prettyName(aID); + }; +} + +/** + * Context menu invoker. + */ +function synthContextMenu(aID, aCheckerOrEventSeq) { + this.__proto__ = new synthClick(aID, aCheckerOrEventSeq, { + button: 0, + type: "contextmenu", + }); + + this.getID = function synthContextMenu_getID() { + return "context menu on " + prettyName(aID); + }; +} + +/** + * Open combobox, autocomplete and etc popup, check expandable states. + */ +function openCombobox(aComboboxID) { + this.eventSeq = [ + new stateChangeChecker(STATE_EXPANDED, false, true, aComboboxID), + ]; + + this.invoke = function openCombobox_invoke() { + getNode(aComboboxID).focus(); + synthesizeKey("VK_DOWN", { altKey: true }); + }; + + this.getID = function openCombobox_getID() { + return "open combobox " + prettyName(aComboboxID); + }; +} + +/** + * Close combobox, autocomplete and etc popup, check expandable states. + */ +function closeCombobox(aComboboxID) { + this.eventSeq = [ + new stateChangeChecker(STATE_EXPANDED, false, false, aComboboxID), + ]; + + this.invoke = function closeCombobox_invoke() { + synthesizeKey("KEY_Escape"); + }; + + this.getID = function closeCombobox_getID() { + return "close combobox " + prettyName(aComboboxID); + }; +} + +/** + * Select all invoker. + */ +function synthSelectAll(aNodeOrID, aCheckerOrEventSeq) { + this.__proto__ = new synthAction(aNodeOrID, aCheckerOrEventSeq); + + this.invoke = function synthSelectAll_invoke() { + if (ChromeUtils.getClassName(this.DOMNode) === "HTMLInputElement") { + this.DOMNode.select(); + } else { + window.getSelection().selectAllChildren(this.DOMNode); + } + }; + + this.getID = function synthSelectAll_getID() { + return aNodeOrID + " selectall"; + }; +} + +/** + * Move the caret to the end of line. + */ +function moveToLineEnd(aID, aCaretOffset) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_RIGHT", + { metaKey: true }, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } else { + this.__proto__ = new synthEndKey( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } + + this.getID = function moveToLineEnd_getID() { + return "move to line end in " + prettyName(aID); + }; +} + +/** + * Move the caret to the end of previous line if any. + */ +function moveToPrevLineEnd(aID, aCaretOffset) { + this.__proto__ = new synthAction( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + + this.invoke = function moveToPrevLineEnd_invoke() { + synthesizeKey("KEY_ArrowUp"); + + if (MAC) { + synthesizeKey("Key_ArrowRight", { metaKey: true }); + } else { + synthesizeKey("KEY_End"); + } + }; + + this.getID = function moveToPrevLineEnd_getID() { + return "move to previous line end in " + prettyName(aID); + }; +} + +/** + * Move the caret to begining of the line. + */ +function moveToLineStart(aID, aCaretOffset) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_LEFT", + { metaKey: true }, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } else { + this.__proto__ = new synthHomeKey( + aID, + new caretMoveChecker(aCaretOffset, true, aID) + ); + } + + this.getID = function moveToLineEnd_getID() { + return "move to line start in " + prettyName(aID); + }; +} + +/** + * Move the caret to begining of the text. + */ +function moveToTextStart(aID) { + if (MAC) { + this.__proto__ = new synthKey( + aID, + "VK_UP", + { metaKey: true }, + new caretMoveChecker(0, true, aID) + ); + } else { + this.__proto__ = new synthKey( + aID, + "VK_HOME", + { ctrlKey: true }, + new caretMoveChecker(0, true, aID) + ); + } + + this.getID = function moveToTextStart_getID() { + return "move to text start in " + prettyName(aID); + }; +} + +/** + * Move the caret in text accessible. + */ +function moveCaretToDOMPoint( + aID, + aDOMPointNodeID, + aDOMPointOffset, + aExpectedOffset, + aFocusTargetID, + aCheckFunc +) { + this.target = getAccessible(aID, [nsIAccessibleText]); + this.DOMPointNode = getNode(aDOMPointNodeID); + this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; + this.focusNode = this.focus ? this.focus.DOMNode : null; + + this.invoke = function moveCaretToDOMPoint_invoke() { + if (this.focusNode) { + this.focusNode.focus(); + } + + var selection = this.DOMPointNode.ownerGlobal.getSelection(); + var selRange = selection.getRangeAt(0); + selRange.setStart(this.DOMPointNode, aDOMPointOffset); + selRange.collapse(true); + + selection.removeRange(selRange); + selection.addRange(selRange); + }; + + this.getID = function moveCaretToDOMPoint_getID() { + return ( + "Set caret on " + + prettyName(aID) + + " at point: " + + prettyName(aDOMPointNodeID) + + " node with offset " + + aDOMPointOffset + ); + }; + + this.finalCheck = function moveCaretToDOMPoint_finalCheck() { + if (aCheckFunc) { + aCheckFunc.call(); + } + }; + + this.eventSeq = [new caretMoveChecker(aExpectedOffset, true, this.target)]; + + if (this.focus) { + this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); + } +} + +/** + * Set caret offset in text accessible. + */ +function setCaretOffset(aID, aOffset, aFocusTargetID) { + this.target = getAccessible(aID, [nsIAccessibleText]); + this.offset = aOffset == -1 ? this.target.characterCount : aOffset; + this.focus = aFocusTargetID ? getAccessible(aFocusTargetID) : null; + + this.invoke = function setCaretOffset_invoke() { + this.target.caretOffset = this.offset; + }; + + this.getID = function setCaretOffset_getID() { + return "Set caretOffset on " + prettyName(aID) + " at " + this.offset; + }; + + this.eventSeq = [new caretMoveChecker(this.offset, true, this.target)]; + + if (this.focus) { + this.eventSeq.push(new asyncInvokerChecker(EVENT_FOCUS, this.focus)); + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue checkers + +/** + * Common invoker checker (see eventSeq of eventQueue). + */ +function invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg, aIsAsync) { + this.type = aEventType; + this.async = aIsAsync; + + this.__defineGetter__("target", invokerChecker_targetGetter); + this.__defineSetter__("target", invokerChecker_targetSetter); + + // implementation details + function invokerChecker_targetGetter() { + if (typeof this.mTarget == "function") { + return this.mTarget.call(null, this.mTargetFuncArg); + } + if (typeof this.mTarget == "string") { + return getNode(this.mTarget); + } + + return this.mTarget; + } + + function invokerChecker_targetSetter(aValue) { + this.mTarget = aValue; + return this.mTarget; + } + + this.__defineGetter__("targetDescr", invokerChecker_targetDescrGetter); + + function invokerChecker_targetDescrGetter() { + if (typeof this.mTarget == "function") { + return this.mTarget.name + ", arg: " + this.mTargetFuncArg; + } + + return prettyName(this.mTarget); + } + + this.mTarget = aTargetOrFunc; + this.mTargetFuncArg = aTargetFuncArg; +} + +/** + * event checker that forces preceeding async events to happen before this + * checker. + */ +function orderChecker() { + // XXX it doesn't actually work to inherit from invokerChecker, but maybe we + // should fix that? + // this.__proto__ = new invokerChecker(null, null, null, false); +} + +/** + * Generic invoker checker for todo events. + */ +function todo_invokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); + this.todo = true; +} + +/** + * Generic invoker checker for unexpected events. + */ +function unexpectedInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); + + this.unexpected = true; +} + +/** + * Common invoker checker for async events. + */ +function asyncInvokerChecker(aEventType, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + aEventType, + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +function focusChecker(aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + EVENT_FOCUS, + aTargetOrFunc, + aTargetFuncArg, + false + ); + + this.unique = true; // focus event must be unique for invoker action + + this.check = function focusChecker_check(aEvent) { + testStates(aEvent.accessible, STATE_FOCUSED); + }; +} + +function nofocusChecker(aID) { + this.__proto__ = new focusChecker(aID); + this.unexpected = true; +} + +/** + * Text inserted/removed events checker. + * @param aFromUser [in, optional] kNotFromUserInput or kFromUserInput + */ +function textChangeChecker( + aID, + aStart, + aEnd, + aTextOrFunc, + aIsInserted, + aFromUser, + aAsync +) { + this.target = getNode(aID); + this.type = aIsInserted ? EVENT_TEXT_INSERTED : EVENT_TEXT_REMOVED; + this.startOffset = aStart; + this.endOffset = aEnd; + this.textOrFunc = aTextOrFunc; + this.async = aAsync; + + this.match = function stextChangeChecker_match(aEvent) { + if ( + !(aEvent instanceof nsIAccessibleTextChangeEvent) || + aEvent.accessible !== getAccessible(this.target) + ) { + return false; + } + + let tcEvent = aEvent.QueryInterface(nsIAccessibleTextChangeEvent); + let modifiedText = + typeof this.textOrFunc === "function" + ? this.textOrFunc() + : this.textOrFunc; + return modifiedText === tcEvent.modifiedText; + }; + + this.check = function textChangeChecker_check(aEvent) { + aEvent.QueryInterface(nsIAccessibleTextChangeEvent); + + var modifiedText = + typeof this.textOrFunc == "function" + ? this.textOrFunc() + : this.textOrFunc; + var modifiedTextLen = + this.endOffset == -1 ? modifiedText.length : aEnd - aStart; + + is( + aEvent.start, + this.startOffset, + "Wrong start offset for " + prettyName(aID) + ); + is(aEvent.length, modifiedTextLen, "Wrong length for " + prettyName(aID)); + var changeInfo = aIsInserted ? "inserted" : "removed"; + is( + aEvent.isInserted, + aIsInserted, + "Text was " + changeInfo + " for " + prettyName(aID) + ); + is( + aEvent.modifiedText, + modifiedText, + "Wrong " + changeInfo + " text for " + prettyName(aID) + ); + if (typeof aFromUser != "undefined") { + is( + aEvent.isFromUserInput, + aFromUser, + "wrong value of isFromUserInput() for " + prettyName(aID) + ); + } + }; +} + +/** + * Caret move events checker. + */ +function caretMoveChecker( + aCaretOffset, + aIsSelectionCollapsed, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync +) { + this.__proto__ = new invokerChecker( + EVENT_TEXT_CARET_MOVED, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync + ); + + this.check = function caretMoveChecker_check(aEvent) { + let evt = aEvent.QueryInterface(nsIAccessibleCaretMoveEvent); + is( + evt.caretOffset, + aCaretOffset, + "Wrong caret offset for " + prettyName(aEvent.accessible) + ); + is( + evt.isSelectionCollapsed, + aIsSelectionCollapsed, + "wrong collapsed value for " + prettyName(aEvent.accessible) + ); + }; +} + +function asyncCaretMoveChecker(aCaretOffset, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new caretMoveChecker( + aCaretOffset, + true, // Caret is collapsed + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +/** + * Text selection change checker. + */ +function textSelectionChecker( + aID, + aStartOffset, + aEndOffset, + aRangeStartContainer, + aRangeStartOffset, + aRangeEndContainer, + aRangeEndOffset +) { + this.__proto__ = new invokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID); + + this.check = function textSelectionChecker_check(aEvent) { + if (aStartOffset == aEndOffset) { + ok(true, "Collapsed selection triggered text selection change event."); + } else { + testTextGetSelection(aID, aStartOffset, aEndOffset, 0); + + // Test selection test range + let selectionRanges = aEvent.QueryInterface( + nsIAccessibleTextSelectionChangeEvent + ).selectionRanges; + let range = selectionRanges.queryElementAt(0, nsIAccessibleTextRange); + is( + range.startContainer, + getAccessible(aRangeStartContainer), + "correct range start container" + ); + is(range.startOffset, aRangeStartOffset, "correct range start offset"); + is(range.endOffset, aRangeEndOffset, "correct range end offset"); + is( + range.endContainer, + getAccessible(aRangeEndContainer), + "correct range end container" + ); + } + }; +} + +/** + * Object attribute changed checker + */ +function objAttrChangedChecker(aID, aAttr) { + this.__proto__ = new invokerChecker(EVENT_OBJECT_ATTRIBUTE_CHANGED, aID); + + this.check = function objAttrChangedChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface( + nsIAccessibleObjectAttributeChangedEvent + ); + } catch (e) { + ok(false, "Object attribute changed event was expected"); + } + + if (!event) { + return; + } + + is( + event.changedAttribute, + aAttr, + "Wrong attribute name of the object attribute changed event." + ); + }; + + this.match = function objAttrChangedChecker_match(aEvent) { + if (aEvent instanceof nsIAccessibleObjectAttributeChangedEvent) { + var scEvent = aEvent.QueryInterface( + nsIAccessibleObjectAttributeChangedEvent + ); + return ( + aEvent.accessible == getAccessible(this.target) && + scEvent.changedAttribute == aAttr + ); + } + return false; + }; +} + +/** + * State change checker. + */ +function stateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync, + aSkipCurrentStateCheck +) { + this.__proto__ = new invokerChecker( + EVENT_STATE_CHANGE, + aTargetOrFunc, + aTargetFuncArg, + aIsAsync + ); + + this.check = function stateChangeChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + } catch (e) { + ok(false, "State change event was expected"); + } + + if (!event) { + return; + } + + is( + event.isExtraState, + aIsExtraState, + "Wrong extra state bit of the statechange event." + ); + isState( + event.state, + aState, + aIsExtraState, + "Wrong state of the statechange event." + ); + is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state"); + + if (aSkipCurrentStateCheck) { + todo(false, "State checking was skipped!"); + return; + } + + var state = aIsEnabled ? (aIsExtraState ? 0 : aState) : 0; + var extraState = aIsEnabled ? (aIsExtraState ? aState : 0) : 0; + var unxpdState = aIsEnabled ? 0 : aIsExtraState ? 0 : aState; + var unxpdExtraState = aIsEnabled ? 0 : aIsExtraState ? aState : 0; + testStates( + event.accessible, + state, + extraState, + unxpdState, + unxpdExtraState + ); + }; + + this.match = function stateChangeChecker_match(aEvent) { + if (aEvent instanceof nsIAccessibleStateChangeEvent) { + var scEvent = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + return ( + aEvent.accessible == getAccessible(this.target) && + scEvent.state == aState + ); + } + return false; + }; +} + +function asyncStateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg +) { + this.__proto__ = new stateChangeChecker( + aState, + aIsExtraState, + aIsEnabled, + aTargetOrFunc, + aTargetFuncArg, + true + ); +} + +/** + * Expanded state change checker. + */ +function expandedStateChecker(aIsEnabled, aTargetOrFunc, aTargetFuncArg) { + this.__proto__ = new invokerChecker( + EVENT_STATE_CHANGE, + aTargetOrFunc, + aTargetFuncArg + ); + + this.check = function expandedStateChecker_check(aEvent) { + var event = null; + try { + var event = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + } catch (e) { + ok(false, "State change event was expected"); + } + + if (!event) { + return; + } + + is(event.state, STATE_EXPANDED, "Wrong state of the statechange event."); + is( + event.isExtraState, + false, + "Wrong extra state bit of the statechange event." + ); + is(event.isEnabled, aIsEnabled, "Wrong state of statechange event state"); + + testStates(event.accessible, aIsEnabled ? STATE_EXPANDED : STATE_COLLAPSED); + }; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event sequances (array of predefined checkers) + +/** + * Event seq for single selection change. + */ +function selChangeSeq(aUnselectedID, aSelectedID) { + if (!aUnselectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ]; + } + + // Return two possible scenarios: depending on widget type when selection is + // moved the the order of items that get selected and unselected may vary. + return [ + [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ], + [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION, aSelectedID), + ], + ]; +} + +/** + * Event seq for item removed form the selection. + */ +function selRemoveSeq(aUnselectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, false, aUnselectedID), + new invokerChecker(EVENT_SELECTION_REMOVE, aUnselectedID), + ]; +} + +/** + * Event seq for item added to the selection. + */ +function selAddSeq(aSelectedID) { + return [ + new stateChangeChecker(STATE_SELECTED, false, true, aSelectedID), + new invokerChecker(EVENT_SELECTION_ADD, aSelectedID), + ]; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private implementation details. +// ////////////////////////////////////////////////////////////////////////////// + +// ////////////////////////////////////////////////////////////////////////////// +// General + +var gA11yEventListeners = {}; +var gA11yEventApplicantsCount = 0; + +var gA11yEventObserver = { + // eslint-disable-next-line complexity + observe: function observe(aSubject, aTopic, aData) { + if (aTopic != "accessible-event") { + return; + } + + var event; + try { + event = aSubject.QueryInterface(nsIAccessibleEvent); + } catch (ex) { + // After a test is aborted (i.e. timed out by the harness), this exception is soon triggered. + // Remove the leftover observer, otherwise it "leaks" to all the following tests. + Services.obs.removeObserver(this, "accessible-event"); + // Forward the exception, with added explanation. + throw new Error( + "[accessible/events.js, gA11yEventObserver.observe] This is expected " + + `if a previous test has been aborted... Initial exception was: [ ${ex} ]` + ); + } + var listenersArray = gA11yEventListeners[event.eventType]; + + var eventFromDumpArea = false; + if (gLogger.isEnabled()) { + // debug stuff + eventFromDumpArea = true; + + var target = event.DOMNode; + var dumpElm = gA11yEventDumpID + ? document.getElementById(gA11yEventDumpID) + : null; + + if (dumpElm) { + var parent = target; + while (parent && parent != dumpElm) { + parent = parent.parentNode; + } + } + + if (!dumpElm || parent != dumpElm) { + var type = eventTypeToString(event.eventType); + var info = "Event type: " + type; + + if (event instanceof nsIAccessibleStateChangeEvent) { + var stateStr = statesToString( + event.isExtraState ? 0 : event.state, + event.isExtraState ? event.state : 0 + ); + info += ", state: " + stateStr + ", is enabled: " + event.isEnabled; + } else if (event instanceof nsIAccessibleTextChangeEvent) { + info += + ", start: " + + event.start + + ", length: " + + event.length + + ", " + + (event.isInserted ? "inserted" : "removed") + + " text: " + + event.modifiedText; + } + + info += ". Target: " + prettyName(event.accessible); + + if (listenersArray) { + info += ". Listeners count: " + listenersArray.length; + } + + if (gLogger.hasFeature("parentchain:" + type)) { + info += "\nParent chain:\n"; + var acc = event.accessible; + while (acc) { + info += " " + prettyName(acc) + "\n"; + acc = acc.parent; + } + } + + eventFromDumpArea = false; + gLogger.log(info); + } + } + + // Do not notify listeners if event is result of event log changes. + if (!listenersArray || eventFromDumpArea) { + return; + } + + for (var index = 0; index < listenersArray.length; index++) { + listenersArray[index].handleEvent(event); + } + }, +}; + +function listenA11yEvents(aStartToListen) { + if (aStartToListen) { + // Add observer when adding the first applicant only. + if (!gA11yEventApplicantsCount++) { + Services.obs.addObserver(gA11yEventObserver, "accessible-event"); + } + } else { + // Remove observer when there are no more applicants only. + // '< 0' case should not happen, but just in case: removeObserver() will throw. + // eslint-disable-next-line no-lonely-if + if (--gA11yEventApplicantsCount <= 0) { + Services.obs.removeObserver(gA11yEventObserver, "accessible-event"); + } + } +} + +function addA11yEventListener(aEventType, aEventHandler) { + if (!(aEventType in gA11yEventListeners)) { + gA11yEventListeners[aEventType] = []; + } + + var listenersArray = gA11yEventListeners[aEventType]; + var index = listenersArray.indexOf(aEventHandler); + if (index == -1) { + listenersArray.push(aEventHandler); + } +} + +function removeA11yEventListener(aEventType, aEventHandler) { + var listenersArray = gA11yEventListeners[aEventType]; + if (!listenersArray) { + return false; + } + + var index = listenersArray.indexOf(aEventHandler); + if (index == -1) { + return false; + } + + listenersArray.splice(index, 1); + + if (!listenersArray.length) { + gA11yEventListeners[aEventType] = null; + delete gA11yEventListeners[aEventType]; + } + + return true; +} + +/** + * Used to dump debug information. + */ +var gLogger = { + /** + * Return true if dump is enabled. + */ + isEnabled: function debugOutput_isEnabled() { + return ( + gA11yEventDumpID || gA11yEventDumpToConsole || gA11yEventDumpToAppConsole + ); + }, + + /** + * Dump information into DOM and console if applicable. + */ + log: function logger_log(aMsg) { + this.logToConsole(aMsg); + this.logToAppConsole(aMsg); + this.logToDOM(aMsg); + }, + + /** + * Log message to DOM. + * + * @param aMsg [in] the primary message + * @param aHasIndent [in, optional] if specified the message has an indent + * @param aPreEmphText [in, optional] the text is colored and appended prior + * primary message + */ + logToDOM: function logger_logToDOM(aMsg, aHasIndent, aPreEmphText) { + if (gA11yEventDumpID == "") { + return; + } + + var dumpElm = document.getElementById(gA11yEventDumpID); + if (!dumpElm) { + ok( + false, + "No dump element '" + gA11yEventDumpID + "' within the document!" + ); + return; + } + + var containerTagName = + ChromeUtils.getClassName(document) == "HTMLDocument" + ? "div" + : "description"; + + var container = document.createElement(containerTagName); + if (aHasIndent) { + container.setAttribute("style", "padding-left: 10px;"); + } + + if (aPreEmphText) { + var inlineTagName = + ChromeUtils.getClassName(document) == "HTMLDocument" + ? "span" + : "description"; + var emphElm = document.createElement(inlineTagName); + emphElm.setAttribute("style", "color: blue;"); + emphElm.textContent = aPreEmphText; + + container.appendChild(emphElm); + } + + var textNode = document.createTextNode(aMsg); + container.appendChild(textNode); + + dumpElm.appendChild(container); + }, + + /** + * Log message to console. + */ + logToConsole: function logger_logToConsole(aMsg) { + if (gA11yEventDumpToConsole) { + dump("\n" + aMsg + "\n"); + } + }, + + /** + * Log message to error console. + */ + logToAppConsole: function logger_logToAppConsole(aMsg) { + if (gA11yEventDumpToAppConsole) { + Services.console.logStringMessage("events: " + aMsg); + } + }, + + /** + * Return true if logging feature is enabled. + */ + hasFeature: function logger_hasFeature(aFeature) { + var startIdx = gA11yEventDumpFeature.indexOf(aFeature); + if (startIdx == -1) { + return false; + } + + var endIdx = startIdx + aFeature.length; + return ( + endIdx == gA11yEventDumpFeature.length || + gA11yEventDumpFeature[endIdx] == ";" + ); + }, +}; + +// ////////////////////////////////////////////////////////////////////////////// +// Sequence + +/** + * Base class of sequence item. + */ +function sequenceItem(aProcessor, aEventType, aTarget, aItemID) { + // private + + this.startProcess = function sequenceItem_startProcess() { + this.queue.invoke(); + }; + + this.queue = new eventQueue(); + this.queue.onFinish = function() { + aProcessor.onProcessed(); + return DO_NOT_FINISH_TEST; + }; + + var invoker = { + invoke: function invoker_invoke() { + return aProcessor.process(); + }, + getID: function invoker_getID() { + return aItemID; + }, + eventSeq: [new invokerChecker(aEventType, aTarget)], + }; + + this.queue.push(invoker); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Event queue invokers + +/** + * Invoker base class for prepare an action. + */ +function synthAction(aNodeOrID, aEventsObj) { + this.DOMNode = getNode(aNodeOrID); + + if (aEventsObj) { + var scenarios = null; + if (aEventsObj instanceof Array) { + if (aEventsObj[0] instanceof Array) { + scenarios = aEventsObj; + } + // scenarios + else { + scenarios = [aEventsObj]; + } // event sequance + } else { + scenarios = [[aEventsObj]]; // a single checker object + } + + for (var i = 0; i < scenarios.length; i++) { + defineScenario(this, scenarios[i]); + } + } + + this.getID = function synthAction_getID() { + return prettyName(aNodeOrID) + " action"; + }; +} diff --git a/accessible/tests/mochitest/events/a11y.ini b/accessible/tests/mochitest/events/a11y.ini new file mode 100644 index 0000000000..076f146ba2 --- /dev/null +++ b/accessible/tests/mochitest/events/a11y.ini @@ -0,0 +1,63 @@ +[DEFAULT] +support-files = + focus.html + scroll.html + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + +[test_announcement.html] +[test_aria_alert.html] +[test_aria_menu.html] +[test_aria_objattr.html] +[test_aria_owns.html] +[test_aria_statechange.html] +[test_attrs.html] +[test_bug1322593.html] +[test_bug1322593-2.html] +[test_caretmove.html] +[test_coalescence.html] +[test_contextmenu.html] +[test_descrchange.html] +[test_dragndrop.html] +[test_flush.html] +[test_focusable_statechange.html] +[test_focus_aria_activedescendant.html] +[test_focus_autocomplete.xhtml] +# Disabled on Linux and Windows due to frequent failures - bug 695019, bug 890795 +skip-if = os == 'win' || os == 'linux' +[test_focus_canvas.html] +[test_focus_contextmenu.xhtml] +[test_focus_controls.html] +[test_focus_doc.html] +[test_focus_general.html] +[test_focus_general.xhtml] +[test_focus_listcontrols.xhtml] +[test_focus_menu.xhtml] +[test_focus_name.html] +[test_focus_removal.html] +[test_focus_selects.html] +[test_focus_tabbox.xhtml] +skip-if = webrender +[test_focus_tree.xhtml] +[test_fromUserInput.html] +[test_label.xhtml] +[test_menu.xhtml] +[test_mutation.html] +[test_namechange.xhtml] +[test_namechange.html] +[test_scroll.xhtml] +[test_scroll_caret.xhtml] +[test_selection.html] +skip-if = os == 'mac' +[test_selection.xhtml] +skip-if = os == 'mac' +[test_selection_aria.html] +[test_statechange.html] +[test_statechange_tabpanels.xhtml] +[test_text.html] +[test_text_alg.html] +[test_textattrchange.html] +[test_textselchange.html] +[test_tree.xhtml] +[test_valuechange.html] +skip-if = os == 'mac' diff --git a/accessible/tests/mochitest/events/docload/a11y.ini b/accessible/tests/mochitest/events/docload/a11y.ini new file mode 100644 index 0000000000..6e014d511c --- /dev/null +++ b/accessible/tests/mochitest/events/docload/a11y.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + docload_wnd.html + !/accessible/tests/mochitest/*.js + +[test_docload_aria.html] +[test_docload_busy.html] +[test_docload_embedded.html] +[test_docload_iframe.html] +[test_docload_root.html] +skip-if = os == 'mac' # bug 1456997 +[test_docload_shutdown.html] +skip-if = os == 'mac' # bug 1456997 diff --git a/accessible/tests/mochitest/events/docload/docload_wnd.html b/accessible/tests/mochitest/events/docload/docload_wnd.html new file mode 100644 index 0000000000..93df1e86d4 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/docload_wnd.html @@ -0,0 +1,37 @@ +<html> +<head> + <title>Accessible events testing for document</title> + <script> + const STATE_BUSY = Ci.nsIAccessibleStates.STATE_BUSY; + + var gService = null; + function waitForDocLoad() { + if (!gService) { + gService = Cc["@mozilla.org/accessibilityService;1"]. + getService(Ci.nsIAccessibilityService); + } + + var accDoc = gService.getAccessibleFor(document); + + var state = {}; + accDoc.getState(state, {}); + if (state.value & STATE_BUSY) { + window.setTimeout(waitForDocLoad, 0); + return; + } + + hideIFrame(); + } + + function hideIFrame() { + var iframe = document.getElementById("iframe"); + gService.getAccessibleFor(iframe.contentDocument); + iframe.style.display = "none"; + } + </script> +</head> + +<body onload="waitForDocLoad();"> + <iframe id="iframe"></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_aria.html b/accessible/tests/mochitest/events/docload/test_docload_aria.html new file mode 100644 index 0000000000..c5fc099918 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_aria.html @@ -0,0 +1,75 @@ +<html> + +<head> + <title>Accessible events testing for ARIA document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../states.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function showARIADialog(aID) { + this.dialogNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, this.dialogNode), + ]; + + this.invoke = function showARIADialog_invoke() { + this.dialogNode.style.display = "block"; + }; + + this.getID = function showARIADialog_getID() { + return "show ARIA dialog"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + var gQueue = null; + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new showARIADialog("dialog")); + gQueue.push(new showARIADialog("document")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=759833" + title="ARIA documents should fire document loading events"> + Mozilla Bug 759833 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="dialog" id="dialog" style="display: none;">It's a dialog</div> + <div role="document" id="document" style="display: none;">It's a document</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_busy.html b/accessible/tests/mochitest/events/docload/test_docload_busy.html new file mode 100644 index 0000000000..37caf306bb --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_busy.html @@ -0,0 +1,83 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../states.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function makeIFrameVisible(aID) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.DOMNode.parentNode), + { + type: EVENT_STATE_CHANGE, + get target() { + return getAccessible("iframe").firstChild; + }, + match(aEvent) { + // The document shouldn't have busy state (the DOM document was + // loaded before its accessible was created). Do this test lately to + // make sure the content of document accessible was created + // initially, prior to this the document accessible keeps busy + // state. The initial creation happens asynchronously after document + // creation, there are no events we could use to catch it. + let { state, isEnabled } = aEvent.QueryInterface(nsIAccessibleStateChangeEvent); + return state & STATE_BUSY && !isEnabled; + }, + }, + ]; + + this.invoke = () => (this.DOMNode.style.visibility = "visible"); + + this.getID = () => + "The accessible for DOM document loaded before it's shown shouldn't have busy state."; + } + + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + const gQueue = new eventQueue(); + gQueue.push(new makeIFrameVisible("iframe")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=658185" + title="The DOM document loaded before it's shown shouldn't have busy state"> + Mozilla Bug 658185 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="testContainer"><iframe id="iframe" src="about:mozilla" style="visibility: hidden;"></iframe></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_embedded.html b/accessible/tests/mochitest/events/docload/test_docload_embedded.html new file mode 100644 index 0000000000..18873dc904 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_embedded.html @@ -0,0 +1,85 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function changeIframeSrc(aIdentifier, aURL, aTitle) { + this.DOMNode = getNode(aIdentifier); + + function getIframeDoc() { + return getAccessible(getNode(aIdentifier).contentDocument); + } + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getAccessible(this.DOMNode)), + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, getIframeDoc), + ]; + + this.invoke = () => (this.DOMNode.src = aURL); + + this.finalCheck = () => + testAccessibleTree(this.DOMNode, { + role: ROLE_INTERNAL_FRAME, + children: [ + { + role: ROLE_DOCUMENT, + name: aTitle, + }, + ], + }); + + this.getID = () => `change iframe src on ${aURL}`; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + const gQueue = new eventQueue(); + gQueue.push(new changeIframeSrc("iframe", "about:license", "Licenses")); + gQueue.push(new changeIframeSrc("iframe", "about:buildconfig", "Build Configuration")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=420845" + title="Fire event_reorder on any embedded frames/iframes whos document has just loaded"> + Mozilla Bug 420845 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=754165" + title="Fire document load events on iframes too"> + Mozilla Bug 754165 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="testContainer"><iframe id="iframe"></iframe></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_iframe.html b/accessible/tests/mochitest/events/docload/test_docload_iframe.html new file mode 100644 index 0000000000..d410ebb7e2 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_iframe.html @@ -0,0 +1,99 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../states.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + const kHide = 1; + const kShow = 2; + const kRemove = 3; + + function morphIFrame(aIdentifier, aAction) { + this.DOMNode = getNode(aIdentifier); + this.IFrameContainerDOMNode = this.DOMNode.parentNode; + + this.eventSeq = [ + new invokerChecker(aAction === kShow ? EVENT_SHOW : EVENT_HIDE, this.DOMNode), + new invokerChecker(EVENT_REORDER, this.IFrameContainerDOMNode), + ]; + + this.invoke = () => { + if (aAction === kRemove) { + this.IFrameContainerDOMNode.removeChild(this.DOMNode); + } else { + this.DOMNode.style.display = aAction === kHide ? "none" : "block"; + } + }; + + this.finalCheck = () => + testAccessibleTree(this.IFrameContainerDOMNode, { + role: ROLE_SECTION, + children: (aAction == kHide || aAction == kRemove) ? [ ] : + [ + { + role: ROLE_INTERNAL_FRAME, + children: [ + { role: ROLE_DOCUMENT }, + ], + }, + ], + }); + + this.getID = () => { + if (aAction === kRemove) { + return "remove iframe"; + } + + return `change display style of iframe to ${aAction === kHide ? "none" : "block"}`; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + const gQueue = new eventQueue(EVENT_REORDER); + gQueue.push(new morphIFrame("iframe", kHide)); + gQueue.push(new morphIFrame("iframe", kShow)); + gQueue.push(new morphIFrame("iframe", kRemove)); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=566103" + title="Reorganize accessible document handling"> + Mozilla Bug 566103 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="testContainer"><iframe id="iframe"></iframe></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_root.html b/accessible/tests/mochitest/events/docload/test_docload_root.html new file mode 100644 index 0000000000..7947327c28 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_root.html @@ -0,0 +1,126 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + let gDialog; + let gDialogDoc; + let gRootAcc; + + function openDialogWnd(aURL) { + // Get application root accessible. + let docAcc = getAccessible(document); + while (docAcc) { + gRootAcc = docAcc; + try { + docAcc = docAcc.parent; + } catch (e) { + ok(false, `Can't get parent for ${prettyName(docAcc)}`); + throw e; + } + } + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, gRootAcc), + // We use a function here to get the target because gDialog isn't set + // yet, but it will be when the function is called. + new invokerChecker(EVENT_FOCUS, () => gDialog.document) + ]; + + this.invoke = () => (gDialog = window.browsingContext.topChromeWindow.openDialog(aURL)); + + this.finalCheck = () => { + const accTree = { + role: ROLE_APP_ROOT, + children: [ + { + role: ROLE_CHROME_WINDOW, + }, + { + role: ROLE_CHROME_WINDOW, + }, + ], + }; + + testAccessibleTree(gRootAcc, accTree); + + gDialogDoc = gDialog.document; + ok(isAccessibleInCache(gDialogDoc), + `The document accessible for '${aURL}' is not in cache!`); + }; + + this.getID = () => `open dialog '${aURL}'`; + } + + function closeDialogWnd() { + this.eventSeq = [ new invokerChecker(EVENT_FOCUS, getAccessible(document)) ]; + + this.invoke = () => { + gDialog.close(); + window.focus(); + }; + + this.finalCheck = () => { + ok(!isAccessibleInCache(gDialogDoc), + `The document accessible for dialog is in cache still!`); + + gDialog = gDialogDoc = gRootAcc = null; + }; + + this.getID = () => "close dialog"; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + // Front end stuff sometimes likes to stuff things in the hidden window(s) + // in which case we should repress all accessibles for those. + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + // Try to create an accessible for the hidden window's document. + let doc = Services.appShell.hiddenDOMWindow.document; + let hiddenDocAcc = gAccService.getAccessibleFor(doc); + ok(!hiddenDocAcc, "hiddenDOMWindow should not have an accessible."); + + const gQueue = new eventQueue(); + gQueue.push(new openDialogWnd("about:about")); + gQueue.push(new closeDialogWnd()); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=506206" + title="Fire event_reorder application root accessible"> + Mozilla Bug 506206 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/events/docload/test_docload_shutdown.html b/accessible/tests/mochitest/events/docload/test_docload_shutdown.html new file mode 100644 index 0000000000..5838b80083 --- /dev/null +++ b/accessible/tests/mochitest/events/docload/test_docload_shutdown.html @@ -0,0 +1,143 @@ +<html> + +<head> + <title>Accessible events testing for document</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../../common.js"></script> + <script type="application/javascript" + src="../../role.js"></script> + <script type="application/javascript" + src="../../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + let gDialog; + let gDialogDoc; + let gRootAcc; + let gIframeDoc; + + function openWndShutdownDoc(aURL) { + // Get application root accessible. + let docAcc = getAccessible(document); + while (docAcc) { + gRootAcc = docAcc; + try { + docAcc = docAcc.parent; + } catch (e) { + ok(false, `Can't get parent for ${prettyName(docAcc)}`); + throw e; + } + } + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, gRootAcc), + { + type: EVENT_HIDE, + get target() { + gDialogDoc = gDialog.document; + const iframe = gDialogDoc.getElementById("iframe"); + gIframeDoc = iframe.contentDocument; + return iframe; + }, + get targetDescr() { + return "inner iframe of docload_wnd.html document"; + }, + }, + ]; + + + this.invoke = () => gDialog = window.browsingContext.topChromeWindow.openDialog(aURL); + + this.finalCheck = () => { + const accTree = { + role: ROLE_APP_ROOT, + children: [ + { + role: ROLE_CHROME_WINDOW, + }, + { + role: ROLE_CHROME_WINDOW, + }, + ], + }; + + testAccessibleTree(gRootAcc, accTree); + // After timeout after event hide for iframe was handled the document + // accessible for iframe's document should no longer be in cache. + ok(!isAccessibleInCache(gIframeDoc), + "The document accessible for iframe is in cache still after iframe hide!"); + ok(isAccessibleInCache(gDialogDoc), + `The document accessible for '${aURL}' is not in cache!`); + }; + + this.getID = () => `open dialog '${aURL}'`; + } + + function closeWndShutdownDoc() { + this.eventSeq = [ new invokerChecker(EVENT_FOCUS, getAccessible(document)) ]; + + this.invoke = () => { + gDialog.close(); + window.focus(); + }; + + this.finalCheck = () => { + ok(!isAccessibleInCache(gDialogDoc), + "The document accessible for dialog is in cache still!"); + // After the window is closed all alive subdocument accessibles should + // be shut down. + ok(!isAccessibleInCache(gIframeDoc), + "The document accessible for iframe is in cache still!"); + + gDialog = gDialogDoc = gRootAcc = gIframeDoc = null; + }; + + this.getID = () => "close dialog"; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + function doTests() { + // Front end stuff sometimes likes to stuff things in the hidden window(s) + // in which case we should repress all accessibles for those. + const {Services} = ChromeUtils.import("resource://gre/modules/Services.jsm"); + + // Try to create an accessible for the hidden window's document. + let doc = Services.appShell.hiddenDOMWindow.document; + let hiddenDocAcc = gAccService.getAccessibleFor(doc); + ok(!hiddenDocAcc, "hiddenDOMWindow should not have an accessible."); + + const gQueue = new eventQueue(); + gQueue.push(new openWndShutdownDoc("../../events/docload/docload_wnd.html")); + gQueue.push(new closeWndShutdownDoc()); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=571459" + title="Shutdown document accessible when presshell goes away"> + Mozilla Bug 571459 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/events/focus.html b/accessible/tests/mochitest/events/focus.html new file mode 100644 index 0000000000..ab055df82c --- /dev/null +++ b/accessible/tests/mochitest/events/focus.html @@ -0,0 +1,10 @@ +<html> + +<head> + <title>editable document</title> +</head> + +<body contentEditable="true"> + editable document +</body> +</html> diff --git a/accessible/tests/mochitest/events/scroll.html b/accessible/tests/mochitest/events/scroll.html new file mode 100644 index 0000000000..562e0a3825 --- /dev/null +++ b/accessible/tests/mochitest/events/scroll.html @@ -0,0 +1,181 @@ +<html> + +<head> + <title>nsIAccessible actions testing for anchors</title> +</head> + +<body> + <p> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + </p> + <a name="link1">link1</a> + + <p style="color: blue"> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + </p> + + <h1 id="heading_1">heading 1</h1> + <p style="color: blue"> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + text text text text text text text text text text text text text text <br> + </p> +</body> +<html> diff --git a/accessible/tests/mochitest/events/test_announcement.html b/accessible/tests/mochitest/events/test_announcement.html new file mode 100644 index 0000000000..eb303e4aa9 --- /dev/null +++ b/accessible/tests/mochitest/events/test_announcement.html @@ -0,0 +1,61 @@ +<html> + +<head> + <title>Announcement event and method testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function doTests() { + let acc = getAccessible("display"); + + 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"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1525980" + title="Introduce announcement event and method"> + Mozilla Bug 1525980 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_alert.html b/accessible/tests/mochitest/events/test_aria_alert.html new file mode 100644 index 0000000000..48f4197b50 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_alert.html @@ -0,0 +1,84 @@ +<html> + +<head> + <title>ARIA alert event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function showAlert(aID) { + this.DOMNode = document.createElement("div"); + + this.invoke = function showAlert_invoke() { + this.DOMNode.setAttribute("role", "alert"); + this.DOMNode.setAttribute("id", aID); + var text = document.createTextNode("alert"); + this.DOMNode.appendChild(text); + document.body.appendChild(this.DOMNode); + }; + + this.getID = function showAlert_getID() { + return "Show ARIA alert " + aID; + }; + } + + function changeAlert(aID) { + this.__defineGetter__("DOMNode", function() { return getNode(aID); }); + + this.invoke = function changeAlert_invoke() { + this.DOMNode.textContent = "new alert"; + }; + + this.getID = function showAlert_getID() { + return "Change ARIA alert " + aID; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuging + // enableLogging("tree,events,verbose"); + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(nsIAccessibleEvent.EVENT_ALERT); + + gQueue.push(new showAlert("alert")); + gQueue.push(new changeAlert("alert")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=591199" + title="mochitest for bug 334386: fire alert event when ARIA alert is shown or new its children are inserted"> + Mozilla Bug 591199 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_menu.html b/accessible/tests/mochitest/events/test_aria_menu.html new file mode 100644 index 0000000000..b240090cb9 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_menu.html @@ -0,0 +1,267 @@ +<html> + +<head> + <title>ARIA menu events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + const kViaDisplayStyle = 0; + const kViaVisibilityStyle = 1; + + function focusMenu(aMenuBarID, aMenuID, aActiveMenuBarID) { + this.eventSeq = []; + + if (aActiveMenuBarID) { + this.eventSeq.push(new invokerChecker(EVENT_MENU_END, + getNode(aActiveMenuBarID))); + } + + this.eventSeq.push(new invokerChecker(EVENT_MENU_START, getNode(aMenuBarID))); + this.eventSeq.push(new invokerChecker(EVENT_FOCUS, getNode(aMenuID))); + + this.invoke = function focusMenu_invoke() { + getNode(aMenuID).focus(); + }; + + this.getID = function focusMenu_getID() { + return "focus menu '" + aMenuID + "'"; + }; + } + + function showMenu(aMenuID, aParentMenuID, aHow) { + this.menuNode = getNode(aMenuID); + + // Because of aria-owns processing we may have menupopup start fired before + // related show event. + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.menuNode), + new invokerChecker(EVENT_REORDER, getNode(aParentMenuID)), + new invokerChecker(EVENT_MENUPOPUP_START, this.menuNode), + ]; + + this.invoke = function showMenu_invoke() { + if (aHow == kViaDisplayStyle) + this.menuNode.style.display = "block"; + else + this.menuNode.style.visibility = "visible"; + }; + + this.getID = function showMenu_getID() { + return "Show ARIA menu '" + aMenuID + "' by " + + (aHow == kViaDisplayStyle ? "display" : "visibility") + + " style tricks"; + }; + } + + function closeMenu(aMenuID, aParentMenuID, aHow) { + this.menuNode = getNode(aMenuID); + this.menu = null; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getMenu, this), + new invokerChecker(EVENT_MENUPOPUP_END, getMenu, this), + new invokerChecker(EVENT_REORDER, getNode(aParentMenuID)), + ]; + + this.invoke = function closeMenu_invoke() { + // Store menu accessible reference while menu is still open. + this.menu = getAccessible(this.menuNode); + + // Hide menu. + if (aHow == kViaDisplayStyle) + this.menuNode.style.display = "none"; + else + this.menuNode.style.visibility = "hidden"; + }; + + this.getID = function closeMenu_getID() { + return "Close ARIA menu " + aMenuID + " by " + + (aHow == kViaDisplayStyle ? "display" : "visibility") + + " style tricks"; + }; + + function getMenu(aThisObj) { + return aThisObj.menu; + } + } + + function focusInsideMenu(aMenuID, aMenuBarID) { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getNode(aMenuID)), + ]; + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_MENU_END, getNode(aMenuBarID)), + ]; + + this.invoke = function focusInsideMenu_invoke() { + getNode(aMenuID).focus(); + }; + + this.getID = function focusInsideMenu_getID() { + return "focus menu '" + aMenuID + "'"; + }; + } + + function blurMenu(aMenuBarID) { + var eventSeq = [ + new invokerChecker(EVENT_MENU_END, getNode(aMenuBarID)), + new invokerChecker(EVENT_FOCUS, getNode("outsidemenu")), + ]; + + this.__proto__ = new synthClick("outsidemenu", eventSeq); + + this.getID = function blurMenu_getID() { + return "blur menu"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuging + // enableLogging("tree,events,verbose"); + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new focusMenu("menubar2", "menu-help")); + gQueue.push(new focusMenu("menubar", "menu-file", "menubar2")); + gQueue.push(new showMenu("menupopup-file", "menu-file", kViaDisplayStyle)); + gQueue.push(new closeMenu("menupopup-file", "menu-file", kViaDisplayStyle)); + gQueue.push(new showMenu("menupopup-edit", "menu-edit", kViaVisibilityStyle)); + gQueue.push(new closeMenu("menupopup-edit", "menu-edit", kViaVisibilityStyle)); + gQueue.push(new focusInsideMenu("menu-edit", "menubar")); + gQueue.push(new blurMenu("menubar")); + + gQueue.push(new focusMenu("menubar3", "mb3-mi-outside")); + gQueue.push(new showMenu("mb4-menu", document, kViaDisplayStyle)); + gQueue.push(new focusMenu("menubar4", "mb4-item1")); + gQueue.push(new focusMenu("menubar5", "mb5-mi")); + + gQueue.push(new synthFocus("mi6")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=606207" + title="Dojo dropdown buttons are broken"> + Bug 606207 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=614829" + title="Menupopup end event isn't fired for ARIA menus"> + Bug 614829 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=615189" + title="Clean up FireAccessibleFocusEvent"> + Bug 615189 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Bug 673958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=933322" + title="menustart/end events are missing when aria-owns makes a menu hierarchy"> + Bug 933322 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=934460" + title="menustart/end events may be missed when top level menuitem is focused"> + Bug 934460 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=970005" + title="infinite long loop in a11y:FocusManager::ProcessFocusEvent"> + Bug 970005 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="menubar" role="menubar"> + <div id="menu-file" role="menuitem" tabindex="0"> + File + <div id="menupopup-file" role="menu" style="display: none;"> + <div id="menuitem-newtab" role="menuitem" tabindex="0">New Tab</div> + <div id="menuitem-newwindow" role="menuitem" tabindex="0">New Window</div> + </div> + </div> + <div id="menu-edit" role="menuitem" tabindex="0"> + Edit + <div id="menupopup-edit" role="menu" style="visibility: hidden;"> + <div id="menuitem-undo" role="menuitem" tabindex="0">Undo</div> + <div id="menuitem-redo" role="menuitem" tabindex="0">Redo</div> + </div> + </div> + </div> + <div id="menubar2" role="menubar"> + <div id="menu-help" role="menuitem" tabindex="0"> + Help + <div id="menupopup-help" role="menu" style="display: none;"> + <div id="menuitem-about" role="menuitem" tabindex="0">About</div> + </div> + </div> + </div> + <div tabindex="0" id="outsidemenu">outsidemenu</div> + + <!-- aria-owns relations --> + <div id="menubar3" role="menubar" aria-owns="mb3-mi-outside"></div> + <div id="mb3-mi-outside" role="menuitem" tabindex="0">Outside</div> + + <div id="menubar4" role="menubar"> + <div id="mb4_topitem" role="menuitem" aria-haspopup="true" + aria-owns="mb4-menu">Item</div> + </div> + <div id="mb4-menu" role="menu" style="display:none;"> + <div role="menuitem" id="mb4-item1" tabindex="0">Item 1.1</div> + <div role="menuitem" tabindex="0">Item 1.2</div> + </div> + + <!-- focus top-level menu item having haspopup --> + <div id="menubar5" role="menubar"> + <div role="menuitem" aria-haspopup="true" id="mb5-mi" tabindex="0"> + Item + <div role="menu" style="display:none;"> + <div role="menuitem" tabindex="0">Item 1.1</div> + <div role="menuitem" tabindex="0">Item 1.2</div> + </div> + </div> + </div> + + <!-- other aria-owns relations --> + <div id="mi6" tabindex="0" role="menuitem">aria-owned item</div> + <div aria-owns="mi6">Obla</div> + + <div id="eventdump"></div> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_objattr.html b/accessible/tests/mochitest/events/test_aria_objattr.html new file mode 100644 index 0000000000..709089ca02 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_objattr.html @@ -0,0 +1,68 @@ +<html> + +<head> + <title>Accessible ARIA object attribute changes</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + /** + * Do tests. + */ + var gQueue = null; + function updateAttribute(aID, aAttr, aValue) { + this.node = getNode(aID); + this.accessible = getAccessible(this.node); + + this.eventSeq = [ + new objAttrChangedChecker(aID, aAttr), + ]; + + this.invoke = function updateAttribute_invoke() { + this.node.setAttribute(aAttr, aValue); + }; + + this.getID = function updateAttribute_getID() { + return aAttr + " for " + aID + " " + aValue; + }; + } + + // gA11yEventDumpToConsole = true; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new updateAttribute("sortable", "aria-sort", "ascending")); + + // For experimental ARIA extensions + gQueue.push(new updateAttribute("custom", "aria-blah", "true")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="sortable" role="columnheader" aria-sort="none">aria-sort</div> + + <div id="custom" role="custom" aria-blah="false">Fat free cheese</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_owns.html b/accessible/tests/mochitest/events/test_aria_owns.html new file mode 100644 index 0000000000..3c638ad838 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_owns.html @@ -0,0 +1,122 @@ +<html> + +<head> + <title>Aria-owns targets shouldn't be on invalidation list so shouldn't have + show/hide events</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Do tests. + + // gA11yEventDumpToConsole = true; // debug stuff + // enableLogging("tree,eventTree,verbose"); + + /** + * Aria-owns target shouldn't have a show event. + * Markup: + * <div id="t1_fc" aria-owns="t1_owns"></div> + * <span id="t1_owns"></div> + */ + function testAriaOwns() { + this.parent = getNode("t1"); + this.fc = document.createElement("div"); + this.fc.setAttribute("id", "t1_fc"); + this.owns = document.createElement("span"); + this.owns.setAttribute("id", "t1_owns"); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.fc), + new unexpectedInvokerChecker(EVENT_SHOW, this.owns), + ]; + + this.invoke = function testAriaOwns_invoke() { + getNode("t1").appendChild(this.fc); + getNode("t1").appendChild(this.owns); + getNode("t1_fc").setAttribute("aria-owns", "t1_owns"); + }; + + this.getID = function testAriaOwns_getID() { + return "Aria-owns target shouldn't have show event"; + }; + } + + /** + * Target of both aria-owns and other aria attribute like aria-labelledby + * shouldn't have a show event. + * Markup: + * <div id="t2_fc" aria-owns="t1_owns"></div> + * <div id="t2_sc" aria-labelledby="t2_owns"></div> + * <span id="t2_owns"></div> + */ + function testAriaOwnsAndLabelledBy() { + this.parent = getNode("t2"); + this.fc = document.createElement("div"); + this.fc.setAttribute("id", "t2_fc"); + this.sc = document.createElement("div"); + this.sc.setAttribute("id", "t2_sc"); + this.owns = document.createElement("span"); + this.owns.setAttribute("id", "t2_owns"); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.fc), + new invokerChecker(EVENT_SHOW, this.sc), + new unexpectedInvokerChecker(EVENT_SHOW, this.owns), + ]; + + this.invoke = function testAriaOwns_invoke() { + getNode("t2").appendChild(this.fc); + getNode("t2").appendChild(this.sc); + getNode("t2").appendChild(this.owns); + getNode("t2_fc").setAttribute("aria-owns", "t2_owns"); + getNode("t2_sc").setAttribute("aria-labelledby", "t2_owns"); + }; + + this.getID = function testAriaOwns_getID() { + return "Aria-owns and aria-labelledby target shouldn't have show event"; + }; + } + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + gQueue.push(new testAriaOwns()); + gQueue.push(new testAriaOwnsAndLabelledBy()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1296420" + title="Aria-owns targets shouldn't be on invalidation list so shouldn't + have show/hide events"> + Mozilla Bug 1296420 + </a><br> + + <div id="testContainer"> + <div id="t1"></div> + + <div id="t2"></div> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_aria_statechange.html b/accessible/tests/mochitest/events/test_aria_statechange.html new file mode 100644 index 0000000000..2ba3516bd2 --- /dev/null +++ b/accessible/tests/mochitest/events/test_aria_statechange.html @@ -0,0 +1,224 @@ +<html> + +<head> + <title>ARIA state change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + + /** + * Do tests. + */ + var gQueue = null; + + // gA11yEventDumpID = "eventdump"; // debugging + // gA11yEventDumpToConsole = true; // debugging + + function expandNode(aID, aIsExpanded) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new expandedStateChecker(aIsExpanded, this.DOMNode), + ]; + + this.invoke = function expandNode_invoke() { + this.DOMNode.setAttribute("aria-expanded", + (aIsExpanded ? "true" : "false")); + }; + + this.getID = function expandNode_getID() { + return prettyName(aID) + " aria-expanded changed to '" + aIsExpanded + "'"; + }; + } + + function busyify(aID, aIsBusy) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new stateChangeChecker(STATE_BUSY, kOrdinalState, aIsBusy, this.DOMNode), + ]; + + this.invoke = function busyify_invoke() { + this.DOMNode.setAttribute("aria-busy", (aIsBusy ? "true" : "false")); + }; + + this.getID = function busyify_getID() { + return prettyName(aID) + " aria-busy changed to '" + aIsBusy + "'"; + }; + } + + function makeCurrent(aID, aIsCurrent, aValue) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new stateChangeChecker(EXT_STATE_CURRENT, true, aIsCurrent, this.DOMNode), + ]; + + this.invoke = function makeCurrent_invoke() { + this.DOMNode.setAttribute("aria-current", aValue); + }; + + this.getID = function makeCurrent_getID() { + return prettyName(aID) + " aria-current changed to " + aValue; + }; + } + + function setAttrOfMixedType(aID, aAttr, aState, aValue) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new stateChangeChecker(aState, kOrdinalState, + aValue == "true", this.DOMNode), + ]; + + if (hasState(aID, STATE_MIXED) || aValue == "mixed") { + this.eventSeq.push( + new stateChangeChecker(STATE_MIXED, kOrdinalState, + aValue == "mixed", this.DOMNode) + ); + } + + this.invoke = function setAttrOfMixedType_invoke() { + this.DOMNode.setAttribute(aAttr, aValue); + }; + + this.getID = function setAttrOfMixedType_getID() { + return prettyName(aID) + " " + aAttr + " changed to '" + aValue + "'"; + }; + } + + function setPressed(aID, aValue) { + this.__proto__ = + new setAttrOfMixedType(aID, "aria-pressed", STATE_PRESSED, aValue); + } + + function setChecked(aID, aValue) { + this.__proto__ = + new setAttrOfMixedType(aID, "aria-checked", STATE_CHECKED, aValue); + } + + function buildQueueForAttr(aList, aQueue, aID, aInvokerFunc) { + for (var i = 0; i < aList.length; i++) { + for (var j = i + 1; j < aList.length; j++) { + // XXX: changes from/to "undefined"/"" shouldn't fire state change + // events, bug 472142. + aQueue.push(new aInvokerFunc(aID, aList[i])); + aQueue.push(new aInvokerFunc(aID, aList[j])); + } + } + } + + function buildQueueForAttrOfMixedType(aQueue, aID, aInvokerFunc) { + var list = [ "", "undefined", "false", "true", "mixed" ]; + buildQueueForAttr(list, aQueue, aID, aInvokerFunc); + } + + function buildQueueForAttrOfBoolType(aQueue, aID, aInvokerFunc) { + var list = [ "", "undefined", "false", "true" ]; + buildQueueForAttr(list, aQueue, aID, aInvokerFunc); + } + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new expandNode("section", true)); + gQueue.push(new expandNode("section", false)); + gQueue.push(new expandNode("div", true)); + gQueue.push(new expandNode("div", false)); + + gQueue.push(new busyify("aria_doc", true)); + gQueue.push(new busyify("aria_doc", false)); + + buildQueueForAttrOfMixedType(gQueue, "pressable", setPressed); + buildQueueForAttrOfMixedType(gQueue, "pressable_native", setPressed); + buildQueueForAttrOfMixedType(gQueue, "checkable", setChecked); + buildQueueForAttrOfBoolType(gQueue, "checkableBool", setChecked); + + gQueue.push(new makeCurrent("current_page_1", false, "false")); + gQueue.push(new makeCurrent("current_page_2", true, "page")); + gQueue.push(new makeCurrent("current_page_2", false, "false")); + gQueue.push(new makeCurrent("current_page_3", true, "true")); + gQueue.push(new makeCurrent("current_page_3", false, "")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=551684" + title="No statechange event for aria-expanded on native HTML elements, is fired on ARIA widgets"> + Mozilla Bug 551684 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=648133" + title="fire state change event for aria-busy"> + Mozilla Bug 648133 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=467143" + title="mixed state change event is fired for focused accessible only"> + Mozilla Bug 467143 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=989958" + title="Pressed state is not exposed on a button element with aria-pressed attribute"> + Mozilla Bug 989958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136563" + title="Support ARIA 1.1 switch role"> + Mozilla Bug 1136563 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1355921" + title="Elements with a defined, non-false value for aria-current should expose ATK_STATE_ACTIVE"> + Mozilla Bug 1355921 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <!-- aria-expanded --> + <div id="section" role="section" aria-expanded="false">expandable section</div> + <div id="div" aria-expanded="false">expandable native div</div> + + <!-- aria-busy --> + <div id="aria_doc" role="document" tabindex="0">A document</div> + + <!-- aria-pressed --> + <div id="pressable" role="button"></div> + <button id="pressable_native"></button> + + <!-- aria-checked --> + <div id="checkable" role="checkbox"></div> + <div id="checkableBool" role="switch"></div> + + <!-- aria-current --> + <div id="current_page_1" role="link" aria-current="page">1</div> + <div id="current_page_2" role="link" aria-current="false">2</div> + <div id="current_page_3" role="link">3</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_attrs.html b/accessible/tests/mochitest/events/test_attrs.html new file mode 100644 index 0000000000..c09bd9cf1e --- /dev/null +++ b/accessible/tests/mochitest/events/test_attrs.html @@ -0,0 +1,85 @@ +<html> + +<head> + <title>Event object attributes tests</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + /** + * Test "event-from-input" object attribute. + */ + function eventFromInputChecker(aEventType, aID, aValue, aNoTargetID) { + this.type = aEventType; + this.target = getAccessible(aID); + + this.noTarget = getNode(aNoTargetID); + + this.check = function checker_check(aEvent) { + testAttrs(aEvent.accessible, { "event-from-input": aValue }, true); + + var accessible = getAccessible(this.noTarget); + testAbsentAttrs(accessible, { "event-from-input": "" }); + }; + } + + /** + * Do tests. + */ + var gQueue = null; + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; // debug stuff + + function doTests() { + gQueue = new eventQueue(); + + var id = "textbox", noTargetId = "textarea"; + let checker = + new eventFromInputChecker(EVENT_FOCUS, id, "false", noTargetId); + gQueue.push(new synthFocus(id, checker)); + + if (!MAC) { // Mac failure is bug 541093 + checker = + new eventFromInputChecker(EVENT_TEXT_CARET_MOVED, id, "true", noTargetId); + gQueue.push(new synthHomeKey(id, checker)); + } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=540285" + title="Event object attributes testing"> + Mozilla Bug 540285 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="textbox" value="hello"> + <textarea id="textarea"></textarea> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_bug1322593-2.html b/accessible/tests/mochitest/events/test_bug1322593-2.html new file mode 100644 index 0000000000..05bd31ffa6 --- /dev/null +++ b/accessible/tests/mochitest/events/test_bug1322593-2.html @@ -0,0 +1,77 @@ +<html> + +<head> + <title>Accessible mutation events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function changeMultipleElements() { + this.node1 = getNode("span1"); + this.node2 = getNode("span2"); + + this.eventSeq = [ + new textChangeChecker("container", 0, 5, "hello", false, undefined, true), + new textChangeChecker("container", 6, 11, "world", false, undefined, true), + new orderChecker(), + new textChangeChecker("container", 0, 1, "a", true, undefined, true), + new textChangeChecker("container", 7, 8, "b", true, undefined, true), + ]; + + this.invoke = function changeMultipleElements_invoke() { + this.node1.textContent = "a"; + this.node2.textContent = "b"; + }; + + this.getID = function changeMultipleElements_invoke_getID() { + return "Change the text content of multiple sibling divs"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests +// gA11yEventDumpToConsole = true; // debugging + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new changeMultipleElements()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322593" + title="missing text change events when multiple elements updated at once"> + Mozilla Bug 1322593 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="container"> + <span id="span1">hello</span> + <span>your</span> + <span id="span2">world</span> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_bug1322593.html b/accessible/tests/mochitest/events/test_bug1322593.html new file mode 100644 index 0000000000..968e808106 --- /dev/null +++ b/accessible/tests/mochitest/events/test_bug1322593.html @@ -0,0 +1,74 @@ +<html> + +<head> + <title>Accessible mutation events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function changeMultipleElements() { + this.node1 = getNode("div1"); + this.node2 = getNode("div2"); + + this.eventSeq = [ + new textChangeChecker("div1", 0, 5, "hello", false, undefined, true), + new textChangeChecker("div2", 0, 5, "world", false, undefined, true), + new orderChecker(), + new textChangeChecker("div1", 0, 1, "a", true, undefined, true), + new textChangeChecker("div2", 0, 1, "b", true, undefined, true), + ]; + + this.invoke = function changeMultipleElements_invoke() { + this.node1.textContent = "a"; + this.node2.textContent = "b"; + }; + + this.getID = function changeMultipleElements_invoke_getID() { + return "Change the text content of multiple sibling divs"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests +// gA11yEventDumpToConsole = true; // debugging + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new changeMultipleElements()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1322593" + title="missing text change events when multiple elements updated at once"> + Mozilla Bug 1322593 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="div1">hello</div> + <div id="div2">world</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_caretmove.html b/accessible/tests/mochitest/events/test_caretmove.html new file mode 100644 index 0000000000..d1091ac7f1 --- /dev/null +++ b/accessible/tests/mochitest/events/test_caretmove.html @@ -0,0 +1,142 @@ +<html> + +<head> + <title>Accessible caret move events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Click checker. + */ + function clickChecker(aCaretOffset, aIsSelectionCollapsed, aID, aExtraNodeOrID, aExtraCaretOffset) { + this.__proto__ = new caretMoveChecker(aCaretOffset, aIsSelectionCollapsed, aID); + + this.extraNode = getNode(aExtraNodeOrID); + + this.check = function clickChecker_check(aEvent) { + this.__proto__.check(aEvent); + + if (this.extraNode) { + var acc = getAccessible(this.extraNode, [nsIAccessibleText]); + is(acc.caretOffset, aExtraCaretOffset, + "Wrong caret offset for " + aExtraNodeOrID); + } + }; + } + + /** + * Do tests. + */ + var gQueue = null; + + // gA11yEventDumpToConsole = true; + + function doTests() { + // test caret move events and caret offsets + gQueue = new eventQueue(); + + var id = "textbox"; + gQueue.push(new synthFocus(id, new caretMoveChecker(5, true, id))); + gQueue.push(new synthSelectAll(id, new caretMoveChecker(5, false, id))); + gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id))); + gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id))); + + if (!MAC) { + gQueue.push(new synthSelectAll(id, new caretMoveChecker(5, false, id))); + gQueue.push(new synthHomeKey(id, new caretMoveChecker(0, true, id))); + gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id))); + } + else { + todo(false, "Make these tests pass on OSX (bug 650294)"); + } + + id = "textarea"; + gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id))); + gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id))); + gQueue.push(new synthDownKey(id, new caretMoveChecker(12, true, id))); + + id = "textarea_wrapped"; + gQueue.push(new setCaretOffset(id, 4, id)); + gQueue.push(new synthLeftKey(id, new caretMoveChecker(4, true, id))); + + id = "p"; + gQueue.push(new synthClick(id, new caretMoveChecker(0, true, id))); + gQueue.push(new synthRightKey(id, new caretMoveChecker(1, true, id))); + gQueue.push(new synthDownKey(id, new caretMoveChecker(6, true, id))); + + id = "p1_in_div"; + gQueue.push(new synthClick(id, new clickChecker(0, true, id, "p2_in_div", -1))); + + id = "p"; + gQueue.push(new synthShiftTab(id, new caretMoveChecker(0, true, id))); + id = "textarea"; + gQueue.push(new synthShiftTab(id, new caretMoveChecker(12, true, id))); + id = "p"; + gQueue.push(new synthTab(id, new caretMoveChecker(0, true, id))); + + // Set caret after a child of span element, i.e. after 'text' text. + gQueue.push(new moveCaretToDOMPoint("test1", getNode("test1_span"), 1, + 4, "test1")); + gQueue.push(new moveCaretToDOMPoint("test2", getNode("test2_span"), 1, + 4, "test2")); + + // empty text element + gQueue.push(new moveCaretToDOMPoint("test3", getNode("test3"), 0, + 0, "test3")); + gQueue.push(new moveCaretToDOMPoint("test4", getNode("test4_span"), 0, + 0, "test4")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=454377" + title="Accessible caret move events testing"> + Bug 454377 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=567571" + title="caret-moved events missing at the end of a wrapped line of text"> + Bug 567571 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=824901" + title="HyperTextAccessible::DOMPointToHypertextOffset fails for node and offset equal to node child count"> + Bug 824901 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="textbox" value="hello"/> + + <textarea id="textarea">text<br>text</textarea> + <p id="p" contentEditable="true"><span>text</span><br/>text</p> + <div id="div" contentEditable="true"><p id="p1_in_div">text</p><p id="p2_in_div">text</p></div> + + <p contentEditable="true" id="test1"><span id="test1_span">text</span>ohoho</p> + <p contentEditable="true" id="test2"><span><span id="test2_span">text</span></span>ohoho</p> + <p contentEditable="true" id="test3"></p> + <p contentEditable="true" id="test4"><span id="test4_span"></span></p> + + <textarea id="textarea_wrapped" cols="5">hey friend</textarea> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_coalescence.html b/accessible/tests/mochitest/events/test_coalescence.html new file mode 100644 index 0000000000..0f8ad52a8b --- /dev/null +++ b/accessible/tests/mochitest/events/test_coalescence.html @@ -0,0 +1,817 @@ +<html> + +<head> + <title>Accessible mutation events coalescence testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invoker base classes + + const kRemoveElm = 1; + const kHideElm = 2; + const kAddElm = 3; + const kShowElm = 4; + + /** + * Base class to test of mutation events coalescence. + */ + function coalescenceBase(aChildAction, aParentAction, + aPerformActionOnChildInTheFirstPlace) { + // Invoker interface + + this.invoke = function coalescenceBase_invoke() { + if (aPerformActionOnChildInTheFirstPlace) { + this.invokeAction(this.childNode, aChildAction); + this.invokeAction(this.parentNode, aParentAction); + } else { + this.invokeAction(this.parentNode, aParentAction); + this.invokeAction(this.childNode, aChildAction); + } + }; + + this.getID = function coalescenceBase_getID() { + var childAction = this.getActionName(aChildAction) + " child"; + var parentAction = this.getActionName(aParentAction) + " parent"; + + if (aPerformActionOnChildInTheFirstPlace) + return childAction + " and then " + parentAction; + + return parentAction + " and then " + childAction; + }; + + this.finalCheck = function coalescenceBase_check() { + if (this.getEventType(aChildAction) == EVENT_HIDE) { + testIsDefunct(this.child); + } + if (this.getEventType(aParentAction) == EVENT_HIDE) { + testIsDefunct(this.parent); + } + }; + + // Implementation details + + this.invokeAction = function coalescenceBase_invokeAction(aNode, aAction) { + switch (aAction) { + case kRemoveElm: + aNode.remove(); + break; + + case kHideElm: + aNode.style.display = "none"; + break; + + case kAddElm: + if (aNode == this.parentNode) + this.hostNode.appendChild(this.parentNode); + else + this.parentNode.appendChild(this.childNode); + break; + + case kShowElm: + aNode.style.display = "block"; + break; + + default: + return INVOKER_ACTION_FAILED; + } + // 0 means the action succeeded. + return 0; + }; + + this.getEventType = function coalescenceBase_getEventType(aAction) { + switch (aAction) { + case kRemoveElm: case kHideElm: + return EVENT_HIDE; + case kAddElm: case kShowElm: + return EVENT_SHOW; + } + return 0; + }; + + this.getActionName = function coalescenceBase_getActionName(aAction) { + switch (aAction) { + case kRemoveElm: + return "remove"; + case kHideElm: + return "hide"; + case kAddElm: + return "add"; + case kShowElm: + return "show"; + default: + return "??"; + } + }; + + this.initSequence = function coalescenceBase_initSequence() { + // expected events + var eventType = this.getEventType(aParentAction); + this.eventSeq = [ + new invokerChecker(eventType, this.parentNode), + new invokerChecker(EVENT_REORDER, this.hostNode), + ]; + + // unexpected events + this.unexpectedEventSeq = [ + new invokerChecker(this.getEventType(aChildAction), this.childNode), + new invokerChecker(EVENT_REORDER, this.parentNode), + ]; + }; + } + + /** + * Remove or hide mutation events coalescence testing. + */ + function removeOrHideCoalescenceBase(aChildID, aParentID, + aChildAction, aParentAction, + aPerformActionOnChildInTheFirstPlace) { + this.__proto__ = new coalescenceBase(aChildAction, aParentAction, + aPerformActionOnChildInTheFirstPlace); + + this.init = function removeOrHideCoalescenceBase_init() { + this.childNode = getNode(aChildID); + this.parentNode = getNode(aParentID); + this.child = getAccessible(this.childNode); + this.parent = getAccessible(this.parentNode); + this.hostNode = this.parentNode.parentNode; + }; + + // Initalization + + this.init(); + this.initSequence(); + } + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + /** + * Remove child node and then its parent node from DOM tree. + */ + function removeChildNParent(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kRemoveElm, kRemoveElm, + true); + } + + /** + * Remove parent node and then its child node from DOM tree. + */ + function removeParentNChild(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kRemoveElm, kRemoveElm, + false); + } + + /** + * Hide child node and then its parent node. + */ + function hideChildNParent(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kHideElm, kHideElm, + true); + } + + /** + * Hide parent node and then its child node. + */ + function hideParentNChild(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kHideElm, kHideElm, + false); + } + + /** + * Hide child node and then remove its parent node. + */ + function hideChildNRemoveParent(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kHideElm, kRemoveElm, + true); + } + + /** + * Hide parent node and then remove its child node. + */ + function hideParentNRemoveChild(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kRemoveElm, kHideElm, + false); + } + + /** + * Remove child node and then hide its parent node. + */ + function removeChildNHideParent(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kRemoveElm, kHideElm, + true); + } + + /** + * Remove parent node and then hide its child node. + */ + function removeParentNHideChild(aChildID, aParentID) { + this.__proto__ = new removeOrHideCoalescenceBase(aChildID, aParentID, + kHideElm, kRemoveElm, + false); + } + + /** + * Create and append parent node and create and append child node to it. + */ + function addParentNChild(aHostID, aPerformActionOnChildInTheFirstPlace) { + this.init = function addParentNChild_init() { + this.hostNode = getNode(aHostID); + this.parentNode = document.createElement("select"); + this.childNode = document.createElement("option"); + this.childNode.textContent = "testing"; + }; + + this.__proto__ = new coalescenceBase(kAddElm, kAddElm, + aPerformActionOnChildInTheFirstPlace); + + this.init(); + this.initSequence(); + } + + /** + * Show parent node and show child node to it. + */ + function showParentNChild(aParentID, aChildID, + aPerformActionOnChildInTheFirstPlace) { + this.init = function showParentNChild_init() { + this.parentNode = getNode(aParentID); + this.hostNode = this.parentNode.parentNode; + this.childNode = getNode(aChildID); + }; + + this.__proto__ = new coalescenceBase(kShowElm, kShowElm, + aPerformActionOnChildInTheFirstPlace); + + this.init(); + this.initSequence(); + } + + /** + * Create and append child node to the DOM and then show parent node. + */ + function showParentNAddChild(aParentID, + aPerformActionOnChildInTheFirstPlace) { + this.init = function showParentNAddChild_init() { + this.parentNode = getNode(aParentID); + this.hostNode = this.parentNode.parentNode; + this.childNode = document.createElement("option"); + this.childNode.textContent = "testing"; + }; + + this.__proto__ = new coalescenceBase(kAddElm, kShowElm, + aPerformActionOnChildInTheFirstPlace); + + this.init(); + this.initSequence(); + } + + /** + * Remove children and parent + */ + function removeGrandChildrenNHideParent(aChild1Id, aChild2Id, aParentId) { + this.child1 = getNode(aChild1Id); + this.child2 = getNode(aChild2Id); + this.parent = getNode(aParentId); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getAccessible(aParentId)), + new invokerChecker(EVENT_REORDER, getNode(aParentId).parentNode), + new unexpectedInvokerChecker(EVENT_HIDE, getAccessible(aChild1Id)), + new unexpectedInvokerChecker(EVENT_HIDE, getAccessible(aChild2Id)), + new unexpectedInvokerChecker(EVENT_REORDER, getAccessible(aParentId)), + ]; + + this.invoke = function removeGrandChildrenNHideParent_invoke() { + this.child1.remove(); + this.child2.remove(); + this.parent.hidden = true; + }; + + this.getID = function removeGrandChildrenNHideParent_getID() { + return "remove grand children of different parents and then hide their grand parent"; + }; + } + + /** + * Remove a child, and then its parent. + */ + function test3() { + this.o = getAccessible("t3_o"); + this.ofc = getAccessible("t3_o").firstChild; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.o), + new invokerChecker(EVENT_REORDER, "t3_lb"), + new unexpectedInvokerChecker(EVENT_HIDE, this.ofc), + new unexpectedInvokerChecker(EVENT_REORDER, this.o), + ]; + + this.invoke = function test3_invoke() { + getNode("t3_o").textContent = ""; + getNode("t3_lb").removeChild(getNode("t3_o")); + }; + + this.finalCheck = function test3_finalCheck() { + testIsDefunct(this.o); + testIsDefunct(this.ofc); + }; + + this.getID = function test3_getID() { + return "remove a child, and then its parent"; + }; + } + + /** + * Remove children, and then a parent of 2nd child. + */ + function test4() { + this.o1 = getAccessible("t4_o1"); + this.o1fc = this.o1.firstChild; + this.o2 = getAccessible("t4_o2"); + this.o2fc = this.o2.firstChild; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.o1fc), + new invokerChecker(EVENT_HIDE, this.o2), + new invokerChecker(EVENT_REORDER, "t4_lb"), + new unexpectedInvokerChecker(EVENT_HIDE, this.o2fc), + new unexpectedInvokerChecker(EVENT_REORDER, this.o1), + new unexpectedInvokerChecker(EVENT_REORDER, this.o2), + ]; + + this.invoke = function test4_invoke() { + getNode("t4_o1").textContent = ""; + getNode("t4_o2").textContent = ""; + getNode("t4_lb").removeChild(getNode("t4_o2")); + }; + + this.finalCheck = function test4_finalCheck() { + testIsDefunct(this.o1fc); + testIsDefunct(this.o2); + testIsDefunct(this.o2fc); + }; + + this.getID = function test4_getID() { + return "remove children, and then a parent of 2nd child"; + }; + } + + /** + * Remove a child, remove a parent sibling, remove the parent + */ + function test5() { + this.o = getAccessible("t5_o"); + this.ofc = this.o.firstChild; + this.b = getAccessible("t5_b"); + this.lb = getAccessible("t5_lb"); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.b), + new invokerChecker(EVENT_HIDE, this.o), + new invokerChecker(EVENT_REORDER, "t5"), + new unexpectedInvokerChecker(EVENT_HIDE, this.ofc), + new unexpectedInvokerChecker(EVENT_REORDER, this.o), + new unexpectedInvokerChecker(EVENT_REORDER, this.lb), + ]; + + this.invoke = function test5_invoke() { + getNode("t5_o").textContent = ""; + getNode("t5").removeChild(getNode("t5_b")); + getNode("t5_lb").removeChild(getNode("t5_o")); + }; + + this.finalCheck = function test5_finalCheck() { + testIsDefunct(this.ofc); + testIsDefunct(this.o); + testIsDefunct(this.b); + }; + + this.getID = function test5_getID() { + return "remove a child, remove a parent sibling, remove the parent"; + }; + } + + /** + * Insert accessibles with a child node moved by aria-owns + * Markup: + * <div id="t6_fc"> + * <div id="t6_owns"></div> + * </div> + * <div id="t6_sc" aria-owns="t6_owns"></div> + */ + function test6() { + this.parent = getNode("t6"); + this.fc = document.createElement("div"); + this.fc.setAttribute("id", "t6_fc"); + this.owns = document.createElement("div"); + this.owns.setAttribute("id", "t6_owns"); + this.sc = document.createElement("div"); + this.sc.setAttribute("id", "t6_sc"); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.fc), + new invokerChecker(EVENT_SHOW, this.sc), + new invokerChecker(EVENT_REORDER, this.parent), + new unexpectedInvokerChecker(EVENT_REORDER, this.fc), + new unexpectedInvokerChecker(EVENT_REORDER, this.sc), + new unexpectedInvokerChecker(EVENT_HIDE, this.owns), + new unexpectedInvokerChecker(EVENT_SHOW, this.owns), + ]; + + this.invoke = function test6_invoke() { + getNode("t6").appendChild(this.fc); + getNode("t6_fc").appendChild(this.owns); + getNode("t6").appendChild(this.sc); + getNode("t6_sc").setAttribute("aria-owns", "t6_owns"); + }; + + this.getID = function test6_getID() { + return "Insert accessibles with a child node moved by aria-owns"; + }; + } + + /** + * Insert text nodes under direct and grand children, and then hide + * their container by means of aria-owns. + * + * Markup: + * <div id="t7_moveplace" aria-owns="t7_c"></div> + * <div id="t7_c"> + * <div id="t7_c_directchild">ha</div> + * <div><div id="t7_c_grandchild">ha</div></div> + * </div> + */ + function test7() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t7_c")), + new invokerChecker(EVENT_SHOW, getNode("t7_c")), + new invokerChecker(EVENT_REORDER, getNode("t7")), + new unexpectedInvokerChecker(EVENT_REORDER, getNode("t7_c_directchild")), + new unexpectedInvokerChecker(EVENT_REORDER, getNode("t7_c_grandchild")), + new unexpectedInvokerChecker(EVENT_SHOW, () => getNode("t7_c_directchild").firstChild), + new unexpectedInvokerChecker(EVENT_SHOW, () => getNode("t7_c_grandchild").firstChild), + ]; + + this.invoke = function test7_invoke() { + getNode("t7_c_directchild").textContent = "ha"; + getNode("t7_c_grandchild").textContent = "ha"; + getNode("t7_moveplace").setAttribute("aria-owns", "t7_c"); + }; + + this.getID = function test7_getID() { + return "Show child accessibles and then hide their container"; + }; + } + + /** + * Move a node by aria-owns from right to left in the tree, so that + * the eventing looks this way: + * reorder for 't8_c1' + * hide for 't8_c1_child' + * show for 't8_c2_moved' + * reorder for 't8_c2' + * hide for 't8_c2_moved' + * + * The hide event should be delivered before the paired show event. + */ + function test8() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t8_c1_child")), + new invokerChecker(EVENT_HIDE, "t8_c2_moved"), + new invokerChecker(EVENT_SHOW, "t8_c2_moved"), + new invokerChecker(EVENT_REORDER, "t8_c2"), + new invokerChecker(EVENT_REORDER, "t8_c1"), + ]; + + this.invoke = function test8_invoke() { + // Remove a node from 't8_c1' container to give the event tree a + // desired structure (the 't8_c1' container node goes first in the event + // tree) + getNode("t8_c1_child").remove(); + // then move 't8_c2_moved' from 't8_c2' to 't8_c1'. + getNode("t8_c1").setAttribute("aria-owns", "t8_c2_moved"); + }; + + this.getID = function test8_getID() { + return "Move a node by aria-owns to left within the tree"; + }; + } + + /** + * Move 't9_c3_moved' node under 't9_c2_moved', and then move 't9_c2_moved' + * node by aria-owns (same as test10 but has different aria-owns + * ordering), the eventing looks same way as in test10: + * reorder for 't9_c1' + * hide for 't9_c1_child' + * show for 't9_c2_moved' + * reorder for 't9_c2' + * hide for 't9_c2_child' + * hide for 't9_c2_moved' + * reorder for 't9_c3' + * hide for 't9_c3_moved' + * + * The hide events for 't9_c2_moved' and 't9_c3_moved' should be delivered + * before the show event for 't9_c2_moved'. + */ + function test9() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t9_c1_child")), + new invokerChecker(EVENT_HIDE, getNode("t9_c2_child")), + new invokerChecker(EVENT_HIDE, "t9_c3_moved"), + new invokerChecker(EVENT_HIDE, "t9_c2_moved"), + new invokerChecker(EVENT_SHOW, "t9_c2_moved"), + new invokerChecker(EVENT_REORDER, "t9_c3"), + new invokerChecker(EVENT_REORDER, "t9_c2"), + new invokerChecker(EVENT_REORDER, "t9_c1"), + new unexpectedInvokerChecker(EVENT_SHOW, "t9_c3_moved"), + ]; + + this.invoke = function test9_invoke() { + // Remove child nodes from 't9_c1' and 't9_c2' containers to give + // the event tree a needed structure ('t9_c1' and 't9_c2' nodes go + // first in the event tree), + getNode("t9_c1_child").remove(); + getNode("t9_c2_child").remove(); + // then do aria-owns magic. + getNode("t9_c2_moved").setAttribute("aria-owns", "t9_c3_moved"); + getNode("t9_c1").setAttribute("aria-owns", "t9_c2_moved"); + }; + + this.getID = function test9_getID() { + return "Move node #1 by aria-owns and then move node #2 into node #1"; + }; + } + + /** + * Move a node 't10_c3_moved' by aria-owns under a node 't10_c2_moved', + * moved by under 't10_1', so that the eventing looks this way: + * reorder for 't10_c1' + * hide for 't10_c1_child' + * show for 't10_c2_moved' + * reorder for 't10_c2' + * hide for 't10_c2_child' + * hide for 't10_c2_moved' + * reorder for 't10_c3' + * hide for 't10_c3_moved' + * + * The hide events for 't10_c2_moved' and 't10_c3_moved' should be delivered + * before the show event for 't10_c2_moved'. + */ + function test10() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t10_c1_child")), + new invokerChecker(EVENT_HIDE, getNode("t10_c2_child")), + new invokerChecker(EVENT_HIDE, getNode("t10_c2_moved")), + new invokerChecker(EVENT_HIDE, getNode("t10_c3_moved")), + new invokerChecker(EVENT_SHOW, getNode("t10_c2_moved")), + new invokerChecker(EVENT_REORDER, "t10_c2"), + new invokerChecker(EVENT_REORDER, "t10_c1"), + new invokerChecker(EVENT_REORDER, "t10_c3"), + ]; + + this.invoke = function test10_invoke() { + // Remove child nodes from 't10_c1' and 't10_c2' containers to give + // the event tree a needed structure ('t10_c1' and 't10_c2' nodes go first + // in the event tree), + getNode("t10_c1_child").remove(); + getNode("t10_c2_child").remove(); + // then do aria-owns stuff. + getNode("t10_c1").setAttribute("aria-owns", "t10_c2_moved"); + getNode("t10_c2_moved").setAttribute("aria-owns", "t10_c3_moved"); + }; + + this.getID = function test10_getID() { + return "Move a node by aria-owns into a node moved by aria-owns to left within the tree"; + }; + } + + /** + * Move a node by aria-owns from right to left in the tree, and then + * move its parent too by aria-owns. No hide event should be fired for + * original node. + */ + function test11() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t11_c1_child")), + new invokerChecker(EVENT_HIDE, getNode("t11_c2")), + new orderChecker(), + new asyncInvokerChecker(EVENT_SHOW, "t11_c2_child"), + new asyncInvokerChecker(EVENT_SHOW, "t11_c2"), + new orderChecker(), + new invokerChecker(EVENT_REORDER, "t11"), + new unexpectedInvokerChecker(EVENT_HIDE, "t11_c2_child"), + new unexpectedInvokerChecker(EVENT_REORDER, "t11_c1"), + new unexpectedInvokerChecker(EVENT_REORDER, "t11_c2"), + new unexpectedInvokerChecker(EVENT_REORDER, "t11_c3"), + ]; + + this.invoke = function test11_invoke() { + // Remove a node from 't11_c1' container to give the event tree a + // desired structure (the 't11_c1' container node goes first in + // the event tree), + getNode("t11_c1_child").remove(); + // then move 't11_c2_moved' from 't11_c2' to 't11_c1', and then move + // 't11_c2' to 't11_c3'. + getNode("t11_c1").setAttribute("aria-owns", "t11_c2_child"); + getNode("t11_c3").setAttribute("aria-owns", "t11_c2"); + }; + + this.getID = function test11_getID() { + return "Move a node by aria-owns to left within the tree"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests. + + gA11yEventDumpToConsole = true; // debug stuff + // enableLogging("eventTree"); + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new removeChildNParent("option1", "select1")); + gQueue.push(new removeParentNChild("option2", "select2")); + gQueue.push(new hideChildNParent("option3", "select3")); + gQueue.push(new hideParentNChild("option4", "select4")); + gQueue.push(new hideChildNRemoveParent("option5", "select5")); + gQueue.push(new hideParentNRemoveChild("option6", "select6")); + gQueue.push(new removeChildNHideParent("option7", "select7")); + gQueue.push(new removeParentNHideChild("option8", "select8")); + + gQueue.push(new addParentNChild("testContainer", false)); + gQueue.push(new addParentNChild("testContainer", true)); + gQueue.push(new showParentNChild("select9", "option9", false)); + gQueue.push(new showParentNChild("select10", "option10", true)); + gQueue.push(new showParentNAddChild("select11", false)); + gQueue.push(new showParentNAddChild("select12", true)); + + gQueue.push(new removeGrandChildrenNHideParent("t1_child1", "t1_child2", "t1_parent")); + gQueue.push(new test3()); + gQueue.push(new test4()); + gQueue.push(new test5()); + gQueue.push(new test6()); + gQueue.push(new test7()); + gQueue.push(new test8()); + gQueue.push(new test9()); + gQueue.push(new test10()); + gQueue.push(new test11()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=513213" + title="coalesce events when new event is appended to the queue"> + Mozilla Bug 513213 + </a><br> + <a target="_blank" + title="Rework accessible tree update code" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"> + Mozilla Bug 570275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="testContainer"> + <select id="select1"> + <option id="option1">option</option> + </select> + <select id="select2"> + <option id="option2">option</option> + </select> + <select id="select3"> + <option id="option3">option</option> + </select> + <select id="select4"> + <option id="option4">option</option> + </select> + <select id="select5"> + <option id="option5">option</option> + </select> + <select id="select6"> + <option id="option6">option</option> + </select> + <select id="select7"> + <option id="option7">option</option> + </select> + <select id="select8"> + <option id="option8">option</option> + </select> + + <select id="select9" style="display: none"> + <option id="option9" style="display: none">testing</option> + </select> + <select id="select10" style="display: none"> + <option id="option10" style="display: none">testing</option> + </select> + <select id="select11" style="display: none"></select> + <select id="select12" style="display: none"></select> + </div> + + <div id="testContainer2"> + <div id="t1_parent"> + <div id="t1_mid1"><div id="t1_child1"></div></div> + <div id="t1_mid2"><div id="t1_child2"></div></div> + </div> + </div> + + <div id="t3"> + <div role="listbox" id="t3_lb"> + <div role="option" id="t3_o">opt</div> + </div> + </div> + + <div id="t4"> + <div role="listbox" id="t4_lb"> + <div role="option" id="t4_o1">opt1</div> + <div role="option" id="t4_o2">opt2</div> + </div> + </div> + + <div id="t5"> + <div role="button" id="t5_b">btn</div> + <div role="listbox" id="t5_lb"> + <div role="option" id="t5_o">opt</div> + </div> + </div> + + <div id="t6"> + </div> + + <div id="t7"> + <div id="t7_moveplace"></div> + <div id="t7_c"> + <div><div id="t7_c_grandchild"></div></div> + <div id="t7_c_directchild"></div> + </div> + </div> + + <div id="t8"> + <div id="t8_c1"><div id="t8_c1_child"></div></div> + <div id="t8_c2"> + <div id="t8_c2_moved"></div> + </div> + </div> + + <div id="t9"> + <div id="t9_c1"><div id="t9_c1_child"></div></div> + <div id="t9_c2"> + <div id="t9_c2_child"></div> + <div id="t9_c2_moved"></div> + </div> + <div id="t9_c3"> + <div id="t9_c3_moved"></div> + </div> + </div> + + <div id="t10"> + <div id="t10_c1"><div id="t10_c1_child"></div></div> + <div id="t10_c2"> + <div id="t10_c2_child"></div> + <div id="t10_c2_moved"></div> + </div> + <div id="t10_c3"> + <div id="t10_c3_moved"></div> + </div> + </div> + + <div id="t11"> + <div id="t11_c1"><div id="t11_c1_child"></div></div> + <div id="t11_c2"><div id="t11_c2_child"></div></div> + <div id="t11_c3"></div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_contextmenu.html b/accessible/tests/mochitest/events/test_contextmenu.html new file mode 100644 index 0000000000..1dca41886e --- /dev/null +++ b/accessible/tests/mochitest/events/test_contextmenu.html @@ -0,0 +1,125 @@ +<html> + +<head> + <title>Context menu tests</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function showContextMenu(aID) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_MENUPOPUP_START, getContextMenuNode()), + ]; + + this.invoke = function showContextMenu_invoke() { + synthesizeMouse(this.DOMNode, 4, 4, { type: "contextmenu", button: 2 }); + }; + + this.getID = function showContextMenu_getID() { + return "show context menu"; + }; + } + + function selectMenuItem() { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getFocusedMenuItem), + ]; + + this.invoke = function selectMenuItem_invoke() { + synthesizeKey("KEY_ArrowDown"); + }; + + this.getID = function selectMenuItem_getID() { + return "select first menuitem"; + }; + } + + function closeContextMenu(aID) { + this.eventSeq = [ + new invokerChecker(EVENT_MENUPOPUP_END, + getAccessible(getContextMenuNode())), + ]; + + this.invoke = function closeContextMenu_invoke() { + synthesizeKey("KEY_Escape"); + }; + + this.getID = function closeContextMenu_getID() { + return "close context menu"; + }; + } + + function getContextMenuNode() { + return getRootAccessible().DOMDocument. + getElementById("contentAreaContextMenu"); + } + + function getFocusedMenuItem() { + var menu = getAccessible(getAccessible(getContextMenuNode())); + for (var idx = 0; idx < menu.childCount; idx++) { + var item = menu.getChildAt(idx); + + if (hasState(item, STATE_FOCUSED)) + return getAccessible(item); + } + return null; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + var gQueue = null; + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new showContextMenu("input")); + gQueue.push(new selectMenuItem()); + gQueue.push(new closeContextMenu()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=580535" + title="Broken accessibility in context menus"> + Mozilla Bug 580535 + </a><br> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_descrchange.html b/accessible/tests/mochitest/events/test_descrchange.html new file mode 100644 index 0000000000..b7cb3e1433 --- /dev/null +++ b/accessible/tests/mochitest/events/test_descrchange.html @@ -0,0 +1,79 @@ +<html> + +<head> + <title>Accessible description change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function setAttr(aID, aAttr, aValue, aChecker) { + this.eventSeq = [ aChecker ]; + this.invoke = function setAttr_invoke() { + getNode(aID).setAttribute(aAttr, aValue); + }; + + this.getID = function setAttr_getID() { + return "set attr '" + aAttr + "', value '" + aValue + "'"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuggin + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new setAttr("tst1", "aria-describedby", "display", + new invokerChecker(EVENT_DESCRIPTION_CHANGE, "tst1"))); + gQueue.push(new setAttr("tst1", "title", "title", + new unexpectedInvokerChecker(EVENT_DESCRIPTION_CHANGE, "tst1"))); + + gQueue.push(new setAttr("tst2", "title", "title", + new invokerChecker(EVENT_NAME_CHANGE, "tst2"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=991969" + title="Event not fired when description changes"> + Bug 991969 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <button id="tst1">btn1</button> + <button id="tst2">btn2</button> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_dragndrop.html b/accessible/tests/mochitest/events/test_dragndrop.html new file mode 100644 index 0000000000..2613a310a2 --- /dev/null +++ b/accessible/tests/mochitest/events/test_dragndrop.html @@ -0,0 +1,106 @@ +<html> + +<head> + <title>Accessible drag and drop event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + /** + * Do tests. + */ + var gQueue = null; + + // aria grabbed invoker + function changeGrabbed(aNodeOrID, aGrabValue) { + this.DOMNode = getNode(aNodeOrID); + + this.invoke = function changeGrabbed_invoke() { + if (aGrabValue != undefined) { + this.DOMNode.setAttribute("aria-grabbed", aGrabValue); + } + }; + + this.check = function changeGrabbed_check() { + testAttrs(aNodeOrID, {"grabbed": aGrabValue}, true); + }; + + this.getID = function changeGrabbed_getID() { + return prettyName(aNodeOrID) + " aria-grabbed changed"; + }; + } + + // aria dropeffect invoker + function changeDropeffect(aNodeOrID, aDropeffectValue) { + this.DOMNode = getNode(aNodeOrID); + + this.invoke = function changeDropeffect_invoke() { + if (aDropeffectValue != undefined) { + this.DOMNode.setAttribute("aria-dropeffect", aDropeffectValue); + } + }; + + this.check = function changeDropeffect_check() { + testAttrs(aNodeOrID, {"dropeffect": aDropeffectValue}, true); + }; + + this.getID = function changeDropeffect_getID() { + return prettyName(aNodeOrID) + " aria-dropeffect changed"; + }; + } + + function doTests() { + // Test aria attribute mutation events + gQueue = new eventQueue(nsIAccessibleEvent.EVENT_OBJECT_ATTRIBUTE_CHANGED); + + let id = "grabbable"; + gQueue.push(new changeGrabbed(id, "true")); + gQueue.push(new changeGrabbed(id, "false")); + todo(false, "uncomment this test when 472142 is fixed."); + // gQueue.push(new changeGrabbed(id, "undefined")); + + id = "dropregion"; + gQueue.push(new changeDropeffect(id, "copy")); + gQueue.push(new changeDropeffect(id, "execute")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=510441" + title="Add support for nsIAccessibleEvent::OBJECT_ATTRIBUTE_CHANGED"> + Mozilla Bug 510441 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <!-- ARIA grabbed --> + <div id="grabbable" role="button" aria-grabbed="foo">button</div> + + <!-- ARIA dropeffect --> + <div id="dropregion" role="region" aria-dropeffect="none">button</div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_flush.html b/accessible/tests/mochitest/events/test_flush.html new file mode 100644 index 0000000000..7d7b60b81e --- /dev/null +++ b/accessible/tests/mochitest/events/test_flush.html @@ -0,0 +1,74 @@ +<html> + +<head> + <title>Flush delayed events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + SimpleTest.expectAssertions(0, 1); + + var gFocusHandler = { + handleEvent(aEvent) { + switch (this.count) { + case 0: + is(aEvent.DOMNode, getNode("input1"), + "Focus event for input1 was expected!"); + getAccessible("input2").takeFocus(); + break; + + case 1: + is(aEvent.DOMNode, getNode("input2"), + "Focus event for input2 was expected!"); + + unregisterA11yEventListener(EVENT_FOCUS, gFocusHandler); + SimpleTest.finish(); + break; + + default: + ok(false, "Wrong focus event!"); + } + + this.count++; + }, + + count: 0, + }; + + function doTests() { + registerA11yEventListener(EVENT_FOCUS, gFocusHandler); + + getAccessible("input1").takeFocus(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=477551" + title="DocAccessible::FlushPendingEvents isn't robust"> + Mozilla Bug 477551 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input1"> + <input id="input2"> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html b/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html new file mode 100644 index 0000000000..5f005bad49 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_aria_activedescendant.html @@ -0,0 +1,260 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=429547 +--> +<head> + <title>aria-activedescendant focus tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + let PromEvents = {}; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js", + PromEvents); + // gA11yEventDumpToConsole = true; // debugging + + function changeARIAActiveDescendant(aContainer, aItem) { + let itemID = aItem instanceof Node ? aItem.id : aItem; + this.eventSeq = [ + new focusChecker(aItem), + ]; + + this.invoke = function changeARIAActiveDescendant_invoke() { + getNode(aContainer).setAttribute("aria-activedescendant", itemID); + }; + + this.getID = function changeARIAActiveDescendant_getID() { + return "change aria-activedescendant on " + itemID; + }; + } + + function clearARIAActiveDescendant(aID) { + this.eventSeq = [ + new focusChecker(aID), + ]; + + this.invoke = function clearARIAActiveDescendant_invoke() { + getNode(aID).removeAttribute("aria-activedescendant"); + }; + + this.getID = function clearARIAActiveDescendant_getID() { + return "clear aria-activedescendant on container " + aID; + }; + } + + /** + * Change aria-activedescendant to an invalid (non-existent) id. + * Ensure that focus is fired on the element itself. + */ + function changeARIAActiveDescendantInvalid(aID, aInvalidID) { + if (!aInvalidID) { + aInvalidID = "invalid"; + } + + this.eventSeq = [ + new focusChecker(aID), + ]; + + this.invoke = function changeARIAActiveDescendant_invoke() { + getNode(aID).setAttribute("aria-activedescendant", aInvalidID); + }; + + this.getID = function changeARIAActiveDescendant_getID() { + return "change aria-activedescendant to invalid id"; + }; + } + + function insertItemNFocus(aID, aNewItemID) { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, aNewItemID), + new focusChecker(aNewItemID), + ]; + + this.invoke = function insertItemNFocus_invoke() { + var container = getNode(aID); + + var itemNode = document.createElement("div"); + itemNode.setAttribute("id", aNewItemID); + itemNode.setAttribute("role", "listitem"); + itemNode.textContent = aNewItemID; + container.appendChild(itemNode); + + container.setAttribute("aria-activedescendant", aNewItemID); + }; + + this.getID = function insertItemNFocus_getID() { + return "insert new node and focus it with ID: " + aNewItemID; + }; + } + + /** + * Change the id of an element to another id which is the target of + * aria-activedescendant. + * If another element already has the desired id, remove it from that + * element first. + * Ensure that focus is fired on the target element which was given the + * desired id. + * @param aFromID The existing id of the target element. + * @param aToID The desired id to be given to the target element. + */ + function moveARIAActiveDescendantID(aFromID, aToID) { + this.eventSeq = [ + new focusChecker(aToID), + ]; + + this.invoke = function moveARIAActiveDescendantID_invoke() { + let orig = document.getElementById(aToID); + if (orig) { + orig.id = ""; + } + document.getElementById(aFromID).id = aToID; + }; + + this.getID = function moveARIAActiveDescendantID_getID() { + return "move aria-activedescendant id " + aToID; + }; + } + + var gQueue = null; + async function doTest() { + gQueue = new eventQueue(); + // Later tests use await. + let queueFinished = new Promise(resolve => { + gQueue.onFinish = function() { + resolve(); + return DO_NOT_FINISH_TEST; + }; + }); + + gQueue.push(new synthFocus("listbox", new focusChecker("item1"))); + gQueue.push(new changeARIAActiveDescendant("listbox", "item2")); + gQueue.push(new changeARIAActiveDescendant("listbox", "item3")); + + gQueue.push(new synthFocus("combobox_entry", new focusChecker("combobox_entry"))); + gQueue.push(new changeARIAActiveDescendant("combobox", "combobox_option2")); + + gQueue.push(new synthFocus("listbox", new focusChecker("item3"))); + gQueue.push(new insertItemNFocus("listbox", "item4")); + + gQueue.push(new clearARIAActiveDescendant("listbox")); + gQueue.push(new changeARIAActiveDescendant("listbox", "item1")); + gQueue.push(new changeARIAActiveDescendantInvalid("listbox", "invalid")); + + gQueue.push(new changeARIAActiveDescendant("listbox", "roaming")); + gQueue.push(new moveARIAActiveDescendantID("roaming2", "roaming")); + gQueue.push(new changeARIAActiveDescendantInvalid("listbox", "roaming3")); + gQueue.push(new moveARIAActiveDescendantID("roaming", "roaming3")); + + gQueue.push(new synthFocus("activedesc_nondesc_input", + new focusChecker("activedesc_nondesc_option"))); + + let shadowRoot = document.getElementById("shadow").shadowRoot; + let shadowListbox = shadowRoot.getElementById("shadowListbox"); + let shadowItem1 = shadowRoot.getElementById("shadowItem1"); + let shadowItem2 = shadowRoot.getElementById("shadowItem2"); + gQueue.push(new synthFocus(shadowListbox, new focusChecker(shadowItem1))); + gQueue.push(new changeARIAActiveDescendant(shadowListbox, shadowItem2)); + + gQueue.invoke(); + await queueFinished; + // Tests beyond this point use await rather than eventQueue. + + info("Testing simultaneous insertion, relocation and aria-activedescendant"); + let comboboxWithHiddenList = getNode("comboboxWithHiddenList"); + let focused = PromEvents.waitForEvent(EVENT_FOCUS, comboboxWithHiddenList); + comboboxWithHiddenList.focus(); + await focused; + testStates(comboboxWithHiddenList, STATE_FOCUSED); + // hiddenList is owned, so unhiding causes insertion and relocation. + getNode("hiddenList").hidden = false; + focused = PromEvents.waitForEvent(EVENT_FOCUS, "hiddenListOption"); + comboboxWithHiddenList.setAttribute("aria-activedescendant", "hiddenListOption"); + await focused; + testStates("hiddenListOption", STATE_FOCUSED); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=429547" + title="Support aria-activedescendant usage in nsIAccesible::TakeFocus()"> + Mozilla Bug 429547 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=761102" + title="Focus may be missed when ARIA active-descendant is changed on active composite widget"> + Mozilla Bug 761102 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="listbox" aria-activedescendant="item1" id="listbox" tabindex="1" + aria-owns="item3"> + <div role="listitem" id="item1">item1</div> + <div role="listitem" id="item2">item2</div> + <div role="listitem" id="roaming">roaming</div> + <div role="listitem" id="roaming2">roaming2</div> + </div> + <div role="listitem" id="item3">item3</div> + + <div role="combobox" id="combobox"> + <input id="combobox_entry"> + <ul> + <li role="option" id="combobox_option1">option1</li> + <li role="option" id="combobox_option2">option2</li> + </ul> + </div> + + <!-- aria-activedescendant targeting a non-descendant --> + <input id="activedesc_nondesc_input" aria-activedescendant="activedesc_nondesc_option"> + <div role="listbox"> + <div role="option" id="activedesc_nondesc_option">option</div> + </div> + + <div id="shadow"></div> + <script> + let host = document.getElementById("shadow"); + let shadow = host.attachShadow({mode: "open"}); + let listbox = document.createElement("div"); + listbox.id = "shadowListbox"; + listbox.setAttribute("role", "listbox"); + listbox.setAttribute("tabindex", "0"); + shadow.appendChild(listbox); + let item = document.createElement("div"); + item.id = "shadowItem1"; + item.setAttribute("role", "option"); + listbox.appendChild(item); + listbox.setAttribute("aria-activedescendant", "shadowItem1"); + item = document.createElement("div"); + item.id = "shadowItem2"; + item.setAttribute("role", "option"); + listbox.appendChild(item); + </script> + + <div id="comboboxWithHiddenList" tabindex="0" role="combobox" aria-owns="hiddenList"> + </div> + <div id="hiddenList" hidden role="listbox"> + <div id="hiddenListOption" role="option"></div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml b/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml new file mode 100644 index 0000000000..69cdac14c5 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_autocomplete.xhtml @@ -0,0 +1,507 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<!-- Firefox searchbar --> +<?xml-stylesheet href="chrome://browser/content/browser.css" + type="text/css"?> +<!-- SeaMonkey searchbar --> +<?xml-stylesheet href="chrome://navigator/content/navigator.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Accessible focus event testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript" + src="../autocomplete.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Hacky stuffs + + // This is the hacks needed to use a searchbar without browser.js. + var BrowserSearch = { + updateOpenSearchBadge() {} + }; + + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function loadFormAutoComplete(aIFrameID) + { + this.iframeNode = getNode(aIFrameID); + this.iframe = getAccessible(aIFrameID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.iframe) + ]; + + this.invoke = function loadFormAutoComplete_invoke() + { + var url = "data:text/html,<html><body><form id='form'>" + + "<input id='input' name='a11ytest-formautocomplete'>" + + "</form></body></html>"; + this.iframeNode.setAttribute("src", url); + } + + this.getID = function loadFormAutoComplete_getID() + { + return "load form autocomplete page"; + } + } + + function initFormAutoCompleteBy(aIFrameID, aAutoCompleteValue) + { + this.iframe = getAccessible(aIFrameID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.iframe) + ]; + + this.invoke = function initFormAutoCompleteBy_invoke() + { + var iframeDOMDoc = getIFrameDOMDoc(aIFrameID); + + var inputNode = iframeDOMDoc.getElementById("input"); + inputNode.value = aAutoCompleteValue; + var formNode = iframeDOMDoc.getElementById("form"); + formNode.submit(); + } + + this.getID = function initFormAutoCompleteBy_getID() + { + return "init form autocomplete by '" + aAutoCompleteValue + "'"; + } + } + + function loadHTML5ListAutoComplete(aIFrameID) + { + this.iframeNode = getNode(aIFrameID); + this.iframe = getAccessible(aIFrameID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.iframe) + ]; + + this.invoke = function loadHTML5ListAutoComplete_invoke() + { + var url = "data:text/html,<html><body>" + + "<datalist id='cities'><option>hello</option><option>hi</option></datalist>" + + "<input id='input' list='cities'>" + + "</body></html>"; + this.iframeNode.setAttribute("src", url); + } + + this.getID = function loadHTML5ListAutoComplete_getID() + { + return "load HTML5 list autocomplete page"; + } + } + + function removeChar(aID, aCheckerOrEventSeq) + { + this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); + + this.invoke = function removeChar_invoke() + { + synthesizeKey("KEY_ArrowLeft", {shiftKey: true}); + synthesizeKey("KEY_Delete"); + } + + this.getID = function removeChar_getID() + { + return "remove char on " + prettyName(aID); + } + } + + function replaceOnChar(aID, aChar, aCheckerOrEventSeq) + { + this.__proto__ = new synthAction(aID, aCheckerOrEventSeq); + + this.invoke = function replaceOnChar_invoke() + { + this.DOMNode.select(); + sendString(aChar); + } + + this.getID = function replaceOnChar_getID() + { + return "replace on char '" + aChar + "' for" + prettyName(aID); + } + } + + function focusOnMouseOver(aIDFunc, aIDFuncArg) + { + this.eventSeq = [ new focusChecker(aIDFunc, aIDFuncArg) ]; + + this.invoke = function focusOnMouseOver_invoke() + { + this.id = aIDFunc(aIDFuncArg); + this.node = getNode(this.id); + this.window = this.node.ownerGlobal; + + if (this.node.localName == "tree") { + var tree = getAccessible(this.node); + var accessible = getAccessible(this.id); + if (tree != accessible) { + var itemX = {}, itemY = {}, treeX = {}, treeY = {}; + accessible.getBounds(itemX, itemY, {}, {}); + tree.getBounds(treeX, treeY, {}, {}); + this.x = itemX.value - treeX.value; + this.y = itemY.value - treeY.value; + } + } + + // Generate mouse move events in timeouts until autocomplete popup list + // doesn't have it, the reason is do that because autocomplete popup + // ignores mousemove events firing in too short range. + synthesizeMouse(this.node, this.x, this.y, { type: "mousemove" }); + this.doMouseMoveFlood(this); + } + + this.finalCheck = function focusOnMouseOver_getID() + { + this.isFlooding = false; + } + + this.getID = function focusOnMouseOver_getID() + { + return "mouse over on " + prettyName(aIDFunc(aIDFuncArg)); + } + + this.doMouseMoveFlood = function focusOnMouseOver_doMouseMoveFlood(aThis) + { + synthesizeMouse(aThis.node, aThis.x + 1, aThis.y + 1, + { type: "mousemove" }, aThis.window); + + if (aThis.isFlooding) + aThis.window.setTimeout(aThis.doMouseMoveFlood, 0, aThis); + } + + this.id = null; + this.node = null; + this.window = null; + + this.isFlooding = true; + this.x = 1; + this.y = 1; + } + + function selectByClick(aIDFunc, aIDFuncArg, + aFocusTargetFunc, aFocusTargetFuncArg) + { + this.eventSeq = [ new focusChecker(aFocusTargetFunc, aFocusTargetFuncArg) ]; + + this.invoke = function selectByClick_invoke() + { + var id = aIDFunc(aIDFuncArg); + var node = getNode(id); + var targetWindow = node.ownerGlobal; + + if (node.localName == "tree") { + var tree = getAccessible(node); + var accessible = getAccessible(id); + if (tree != accessible) { + var itemX = {}, itemY = {}, treeX = {}, treeY = {}; + accessible.getBounds(itemX, itemY, {}, {}); + tree.getBounds(treeX, treeY, {}, {}); + this.x = itemX.value - treeX.value; + this.y = itemY.value - treeY.value; + } + } + + synthesizeMouseAtCenter(node, {}, targetWindow); + } + + this.getID = function selectByClick_getID() + { + return "select by click " + prettyName(aIDFunc(aIDFuncArg)); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Target getters + + function getItem(aItemObj) + { + var autocompleteNode = aItemObj.autocompleteNode; + + // XUL searchbar + if (autocompleteNode.localName == "searchbar") { + let popupNode = autocompleteNode._popup; + if (popupNode) { + let list = getAccessible(popupNode); + return list.getChildAt(aItemObj.index); + } + } + + // XUL autocomplete + let popupNode = autocompleteNode.popup; + if (!popupNode) { + // HTML form autocomplete + var controller = Cc["@mozilla.org/autocomplete/controller;1"]. + getService(Ci.nsIAutoCompleteController); + popupNode = controller.input.popup; + } + + if (popupNode) { + if ("richlistbox" in popupNode) { + let list = getAccessible(popupNode.richlistbox); + return list.getChildAt(aItemObj.index); + } + + let list = getAccessible(popupNode.tree); + return list.getChildAt(aItemObj.index + 1); + } + return null; + } + + function getTextEntry(aID) + { + // For form autocompletes the autocomplete widget and text entry widget + // is the same widget, for XUL autocompletes the text entry is a first + // child. + var localName = getNode(aID).localName; + + // HTML form autocomplete + if (localName == "input") + return getAccessible(aID); + + // XUL searchbar + if (localName == "searchbar") + return getAccessible(getNode(aID).textbox); + + return null; + } + + function itemObj(aID, aIdx) + { + this.autocompleteNode = getNode(aID); + + this.autocomplete = this.autocompleteNode.localName == "searchbar" ? + getAccessible(this.autocompleteNode.textbox) : + getAccessible(this.autocompleteNode); + + this.index = aIdx; + } + + function getIFrameDOMDoc(aIFrameID) + { + return getNode(aIFrameID).contentDocument; + } + + //////////////////////////////////////////////////////////////////////////// + // Test helpers + + function queueAutoCompleteTests(aID) + { + // focus autocomplete text entry + gQueue.push(new synthFocus(aID, new focusChecker(getTextEntry, aID))); + + // open autocomplete popup + gQueue.push(new synthDownKey(aID, new nofocusChecker())); + + // select second option ('hi' option), focus on it + gQueue.push(new synthUpKey(aID, + new focusChecker(getItem, new itemObj(aID, 1)))); + + // choose selected option, focus on text entry + gQueue.push(new synthEnterKey(aID, new focusChecker(getTextEntry, aID))); + + // remove char, autocomplete popup appears + gQueue.push(new removeChar(aID, new nofocusChecker())); + + // select first option ('hello' option), focus on it + gQueue.push(new synthDownKey(aID, + new focusChecker(getItem, new itemObj(aID, 0)))); + + // mouse move on second option ('hi' option), focus on it + gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 1))); + + // autocomplete popup updated (no selected item), focus on textentry + gQueue.push(new synthKey(aID, "e", null, new focusChecker(getTextEntry, aID))); + + // select first option ('hello' option), focus on it + gQueue.push(new synthDownKey(aID, + new focusChecker(getItem, new itemObj(aID, 0)))); + + // popup gets hidden, focus on textentry + gQueue.push(new synthRightKey(aID, new focusChecker(getTextEntry, aID))); + + // popup gets open, no focus + gQueue.push(new synthOpenComboboxKey(aID, new nofocusChecker())); + + // select first option again ('hello' option), focus on it + gQueue.push(new synthDownKey(aID, + new focusChecker(getItem, new itemObj(aID, 0)))); + + // no option is selected, focus on text entry + gQueue.push(new synthUpKey(aID, new focusChecker(getTextEntry, aID))); + + // close popup, no focus + gQueue.push(new synthEscapeKey(aID, new nofocusChecker())); + + // autocomplete popup appears (no selected item), focus stays on textentry + gQueue.push(new replaceOnChar(aID, "h", new nofocusChecker())); + + // mouse move on first option ('hello' option), focus on it + gQueue.push(new focusOnMouseOver(getItem, new itemObj(aID, 0))); + + // click first option ('hello' option), popup closes, focus on text entry + gQueue.push(new selectByClick(getItem, new itemObj(aID, 0), getTextEntry, aID)); + } + + //////////////////////////////////////////////////////////////////////////// + // Tests + + //gA11yEventDumpToConsole = true; // debug stuff + + var gInitQueue = null; + function initTests() + { + if (SEAMONKEY || MAC) { + todo(false, "Skipping this test on SeaMonkey ftb. (Bug 718237), and on Mac (bug 746177)"); + shutdownAutoComplete(); + SimpleTest.finish(); + return; + } + + gInitQueue = new eventQueue(); + gInitQueue.push(new loadFormAutoComplete("iframe")); + gInitQueue.push(new initFormAutoCompleteBy("iframe", "hello")); + gInitQueue.push(new initFormAutoCompleteBy("iframe", "hi")); + gInitQueue.push(new loadHTML5ListAutoComplete("iframe2")); + gInitQueue.onFinish = function initQueue_onFinish() + { + SimpleTest.executeSoon(doTests); + return DO_NOT_FINISH_TEST; + } + gInitQueue.invoke(); + } + + var gQueue = null; + function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + + //////////////////////////////////////////////////////////////////////////// + // tree popup autocomplete tests + queueAutoCompleteTests("autocomplete"); + + //////////////////////////////////////////////////////////////////////////// + // richlistbox popup autocomplete tests + queueAutoCompleteTests("richautocomplete"); + + //////////////////////////////////////////////////////////////////////////// + // HTML form autocomplete tests + queueAutoCompleteTests(getIFrameDOMDoc("iframe").getElementById("input")); + + //////////////////////////////////////////////////////////////////////////// + // HTML5 list autocomplete tests + queueAutoCompleteTests(getIFrameDOMDoc("iframe2").getElementById("input")); + + //////////////////////////////////////////////////////////////////////////// + // searchbar tests + + // focus searchbar, focus on text entry + gQueue.push(new synthFocus("searchbar", + new focusChecker(getTextEntry, "searchbar"))); + // open search engine popup, no focus + gQueue.push(new synthOpenComboboxKey("searchbar", new nofocusChecker())); + // select first item, focus on it + gQueue.push(new synthDownKey("searchbar", + new focusChecker(getItem, new itemObj("searchbar", 0)))); + // mouse over on second item, focus on it + gQueue.push(new focusOnMouseOver(getItem, new itemObj("searchbar", 1))); + // press enter key, focus on text entry + gQueue.push(new synthEnterKey("searchbar", + new focusChecker(getTextEntry, "searchbar"))); + // click on search button, open popup, focus goes to document + var searchBtn = getAccessible(getNode("searchbar").searchButton); + gQueue.push(new synthClick(searchBtn, new focusChecker(document))); + // select first item, focus on it + gQueue.push(new synthDownKey("searchbar", + new focusChecker(getItem, new itemObj("searchbar", 0)))); + // close popup, focus goes on document + gQueue.push(new synthEscapeKey("searchbar", new focusChecker(document))); + + gQueue.onFinish = function() + { + // unregister 'test-a11y-search' autocomplete search + shutdownAutoComplete(); + } + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + // Register 'test-a11y-search' autocomplete search. + // XPFE AutoComplete needs to register early. + initAutoComplete([ "hello", "hi" ], + [ "Beep beep'm beep beep yeah", "Baby you can drive my car" ]); + + addA11yLoadEvent(initTests); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=383759" + title="Focus event inconsistent for search box autocomplete"> + Mozilla Bug 383759 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559766" + title="Add accessibility support for @list on HTML input and for HTML datalist"> + Mozilla Bug 559766 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <html:input is="autocomplete-input" + id="autocomplete" + autocompletesearch="test-a11y-search"/> + + <html:input is="autocomplete-input" + id="richautocomplete" + autocompletesearch="test-a11y-search" + autocompletepopup="richpopup"/> + <panel is="autocomplete-richlistbox-popup" + id="richpopup" + type="autocomplete-richlistbox" + noautofocus="true"/> + + <iframe id="iframe"/> + + <iframe id="iframe2"/> + + <searchbar id="searchbar"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_canvas.html b/accessible/tests/mochitest/events/test_focus_canvas.html new file mode 100644 index 0000000000..e2464e41a6 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_canvas.html @@ -0,0 +1,58 @@ +<html> + +<head> + <title>Accessible focus testing in canvas subdom</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new synthFocus("button")); + gQueue.push(new synthTab("button", new focusChecker("textbox"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + <a target="_blank" + title="Expose content in Canvas element" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=495912"> + Mozilla Bug 495912 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <canvas> + <input id="button" type="button"> + <input id="textbox"> + </canvas> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml b/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml new file mode 100644 index 0000000000..c26130dc2e --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_contextmenu.xhtml @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Context nenu focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + //gA11yEventDumpID = "eventdump"; // debug stuff + //gA11yEventDumpToConsole = true; // debug stuff + + var winLowerThanVista = navigator.platform.indexOf("Win") == 0; + if (winLowerThanVista) { + var version = Services.sysinfo.getProperty("version"); + version = parseFloat(version); + winLowerThanVista = !(version >= 6.0); + } + + var gQueue = null; + function doTests() + { + // bug 746183 - Whole file times out on OS X + if (MAC || winLowerThanVista) { + todo(false, "Reenable on mac after fixing bug 746183!"); + SimpleTest.finish(); + return; + } + + // Test focus events. + gQueue = new eventQueue(); + + gQueue.push(new synthFocus("button")); + gQueue.push(new synthContextMenu("button", + new invokerChecker(EVENT_MENUPOPUP_START, "contextmenu"))); + gQueue.push(new synthEscapeKey("contextmenu", new focusChecker("button"))); + + gQueue.push(new synthContextMenu("button", + new invokerChecker(EVENT_MENUPOPUP_START, "contextmenu"))); + gQueue.push(new synthDownKey("contextmenu", new focusChecker("item1"))); + gQueue.push(new synthDownKey("item1", new focusChecker("item2"))); + gQueue.push(new synthRightKey("item2", new focusChecker("item2.1"))); + if (WIN) { + todo(false, "synthEscapeKey for item2.1 and item2 disabled due to bug 691580"); + } else { + gQueue.push(new synthEscapeKey("item2.1", new focusChecker("item2"))); + gQueue.push(new synthEscapeKey("item2", new focusChecker("button"))); + } + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <button id="button" context="contextmenu" label="button"/> + <menupopup id="contextmenu"> + <menuitem id="item1" label="item1"/> + <menu id="item2" label="item2"> + <menupopup> + <menuitem id="item2.1" label="item2.1"/> + </menupopup> + </menu> + </menupopup> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_controls.html b/accessible/tests/mochitest/events/test_focus_controls.html new file mode 100644 index 0000000000..4d25e78908 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_controls.html @@ -0,0 +1,76 @@ +<html> + +<head> + <title>Accessible focus testing on HTML controls</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(EVENT_FOCUS); + + gQueue.push(new synthFocus("textbox")); + gQueue.push(new synthFocus("textarea")); + gQueue.push(new synthFocus("button1")); + gQueue.push(new synthFocus("button2")); + gQueue.push(new synthFocus("checkbox")); + gQueue.push(new synthFocus("radio1")); + gQueue.push(new synthDownKey("radio1", new focusChecker("radio2"))); + + // no focus events for checkbox or radio inputs when they are checked + // programmatically + gQueue.push(new changeCurrentItem("checkbox")); + gQueue.push(new changeCurrentItem("radio1")); + + let fileBrowseButton = getAccessible("file").firstChild; + gQueue.push(new synthFocus("file", new focusChecker(fileBrowseButton))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="textbox"> + <textarea id="textarea"></textarea> + + <input id="button1" type="button" value="button"> + <button id="button2">button</button> + <input id="checkbox" type="checkbox"> + <input id="radio1" type="radio" name="radiogroup"> + <input id="radio2" type="radio" name="radiogroup"> + <input id="file" type="file"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_doc.html b/accessible/tests/mochitest/events/test_focus_doc.html new file mode 100644 index 0000000000..a35fc06ed0 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_doc.html @@ -0,0 +1,92 @@ +<html> + +<head> + <title>Accessible document focus event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + var gQueue = null; + + // var gA11yEventDumpID = "eventdump"; + // gA11yEventDumpToConsole = true; + + function doTests() { + // setup + var frameDoc = document.getElementById("iframe").contentDocument; + frameDoc.designMode = "on"; + var frameDocAcc = getAccessible(frameDoc, [nsIAccessibleDocument]); + var buttonAcc = getAccessible("b1"); + + var frame2Doc = document.getElementById("iframe2").contentDocument; + var frame2Input = frame2Doc.getElementById("input"); + var frame2DocAcc = getAccessible(frame2Doc); + var frame2InputAcc = getAccessible(frame2Input); + + // Test focus events. + gQueue = new eventQueue(); + + // try to give focus to contentEditable frame twice to cover bug 512059 + gQueue.push(new synthFocus(buttonAcc)); + gQueue.push(new synthTab(frameDocAcc, new focusChecker(frameDocAcc))); + gQueue.push(new synthFocus(buttonAcc)); + gQueue.push(new synthTab(frameDocAcc, new focusChecker(frameDocAcc))); + + // focus on not editable document + gQueue.push(new synthFocus(frame2InputAcc)); + gQueue.push(new synthShiftTab(frame2DocAcc, new focusChecker(frame2DocAcc))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512058" + title="Can't set focus to designMode document via accessibility APIs"> + Mozilla Bug 512058 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512059" + title="Accessibility focus event never fired for designMode document after the first focus"> + Mozilla Bug 512059 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=618046" + title="No focus change event when Shift+Tab at top of screen"> + Mozilla Bug 618046 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="eventdump"></div> + + <div id="testContainer"> + <button id="b1">a button</button> + <iframe id="iframe" src="about:blank"></iframe> + <button id="b2">a button</button> + <iframe id="iframe2" src="data:text/html,<html><input id='input'></html>"></iframe> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_general.html b/accessible/tests/mochitest/events/test_focus_general.html new file mode 100644 index 0000000000..abf6a53085 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_general.html @@ -0,0 +1,164 @@ +<html> + +<head> + <title>Accessible focus testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function focusElmWhileSubdocIsFocused(aID) { + this.DOMNode = getNode(aID); + + this.invoke = function focusElmWhileSubdocIsFocused_invoke() { + this.DOMNode.focus(); + }; + + this.eventSeq = [ + new focusChecker(this.DOMNode), + ]; + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_FOCUS, this.DOMNode.ownerDocument), + ]; + + this.getID = function focusElmWhileSubdocIsFocused_getID() { + return "Focus element while subdocument is focused " + prettyName(aID); + }; + } + + function imageMapChecker(aID) { + var node = getNode(aID); + this.type = EVENT_FOCUS; + this.match = function imageMapChecker_match(aEvent) { + return aEvent.DOMNode == node; + }; + } + + function topMenuChecker() { + this.type = EVENT_FOCUS; + this.match = function topMenuChecker_match(aEvent) { + return aEvent.accessible.role == ROLE_PARENT_MENUITEM; + }; + } + + function contextMenuChecker() { + this.type = EVENT_MENUPOPUP_START; + this.match = function contextMenuChecker_match(aEvent) { + return aEvent.accessible.role == ROLE_MENUPOPUP; + }; + } + + function focusContextMenuItemChecker() { + this.__proto__ = new focusChecker(); + + this.match = function focusContextMenuItemChecker_match(aEvent) { + return aEvent.accessible.role == ROLE_MENUITEM; + }; + } + + /** + * Do tests. + */ + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTests() { + var frameDoc = document.getElementById("iframe").contentDocument; + + var editableDoc = document.getElementById("editabledoc").contentDocument; + editableDoc.designMode = "on"; + + gQueue = new eventQueue(); + + gQueue.push(new synthFocus("editablearea")); + gQueue.push(new synthFocus("navarea")); + gQueue.push(new synthTab("navarea", new focusChecker(frameDoc))); + gQueue.push(new focusElmWhileSubdocIsFocused("link")); + + gQueue.push(new synthTab(editableDoc, new focusChecker(editableDoc))); + if (WIN || LINUX) { + // Alt key is used to active menubar and focus menu item on Windows, + // other platforms requires setting a ui.key.menuAccessKeyFocuses + // preference. + gQueue.push(new toggleTopMenu(editableDoc, new topMenuChecker())); + gQueue.push(new toggleTopMenu(editableDoc, new focusChecker(editableDoc))); + } + gQueue.push(new synthContextMenu(editableDoc, new contextMenuChecker())); + gQueue.push(new synthDownKey(editableDoc, new focusContextMenuItemChecker())); + gQueue.push(new synthEscapeKey(editableDoc, new focusChecker(editableDoc))); + if (SEAMONKEY) { + todo(false, "shift tab from editable document fails on (Windows) SeaMonkey! (Bug 718235)"); + } else if (LINUX || MAC) { + todo(false, "shift tab from editable document fails on linux and Mac, bug 746519!"); + } else { + gQueue.push(new synthShiftTab("link", new focusChecker("link"))); + } // ! SEAMONKEY + + gQueue.push(new synthFocus("a", new imageMapChecker("a"))); + gQueue.push(new synthFocus("b", new imageMapChecker("b"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=352220" + title="Inconsistent focus events when returning to a document frame"> + Mozilla Bug 352220 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=550338" + title="Broken focus when returning to editable documents from menus"> + Mozilla Bug 550338 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=961696" + title="Accessible object:state-changed:focused events for imagemap links are broken"> + Mozilla Bug 961696 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="editablearea" contentEditable="true">editable area</div> + <div id="navarea" tabindex="0">navigable area</div> + <iframe id="iframe" src="data:text/html,<html></html>"></iframe> + <a id="link" href="">link</a> + <iframe id="editabledoc" src="about:blank"></iframe> + + <map name="atoz_map"> + <area id="a" coords="0,0,13,14" shape="rect"> + <area id="b" coords="17,0,30,14" shape="rect"> + </map> + <img width="447" height="15" usemap="#atoz_map" src="../letters.gif"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_general.xhtml b/accessible/tests/mochitest/events/test_focus_general.xhtml new file mode 100644 index 0000000000..f3f9a014c1 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_general.xhtml @@ -0,0 +1,118 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Accessible focus event testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + //gA11yEventDumpID = "eventdump"; // debug stuff + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + + gQueue.push(new synthFocus("textbox", + new focusChecker(getNode("textbox")))); + gQueue.push(new synthFocusOnFrame("editabledoc")); + gQueue.push(new synthFocus("radioclothes", + new focusChecker("radiosweater"))); + gQueue.push(new synthDownKey("radiosweater", + new focusChecker("radiojacket"))); + gQueue.push(new synthFocus("checkbox")); + gQueue.push(new synthFocus("button")); + gQueue.push(new synthFocus("checkbutton")); + gQueue.push(new synthFocus("radiobutton")); + + // focus menubutton + gQueue.push(new synthFocus("menubutton")); + // click menubutton, open popup, focus stays on menu button + gQueue.push(new synthClick("menubutton", new nofocusChecker())); + // select first menu item ("item 1"), focus on menu item + gQueue.push(new synthDownKey("menubutton", new focusChecker("mb_item1"))); + // choose select menu item, focus gets back to menubutton + gQueue.push(new synthEnterKey("mb_item1", new focusChecker("menubutton"))); + // press enter to open popup, focus stays on menubutton + gQueue.push(new synthEnterKey("menubutton", new nofocusChecker())); + // select second menu item ("item 2"), focus on menu item + gQueue.push(new synthUpKey("menubutton", new focusChecker("mb_item2"))); + + // clicking on button having associated popup doesn't change a focus + gQueue.push(new synthClick("popupbutton", new nofocusChecker())); + // select first menu item ("item 1"), focus on menu item + gQueue.push(new synthDownKey("popupbutton", new focusChecker("bp_item1"))); + // choose select menu item, focus gets back to menubutton + gQueue.push(new synthEnterKey("bp_item1", new focusChecker("menubutton"))); + // show popup again for the next test + gQueue.push(new synthClick("popupbutton", new nofocusChecker())); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=552368" + title=" fire focus event on document accessible whenever the root or body element is focused"> + Mozilla Bug 552368 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <html:input id="textbox" value="hello"/> + <iframe id="editabledoc" src="focus.html"/> + <radiogroup id="radioclothes"> + <radio id="radiosweater" label="radiosweater"/> + <radio id="radiocap" label="radiocap" disabled="true"/> + <radio id="radiojacket" label="radiojacket"/> + </radiogroup> + <checkbox id="checkbox" label="checkbox"/> + <button id="button" label="button"/> + + <button id="menubutton" type="menu" label="menubutton"> + <menupopup> + <menuitem id="mb_item1" label="item1"/> + <menuitem id="mb_item2" label="item2"/> + </menupopup> + </button> + + <button id="checkbutton" type="checkbox" label="checkbutton"/> + <button id="radiobutton" type="radio" group="rbgroup" label="radio1"/> + + <popupset> + <menupopup id="backpopup" position="after_start"> + <menuitem id="bp_item1" label="Page 1"/> + <menuitem id="bp_item2" label="Page 2"/> + </menupopup> + </popupset> + <button id="popupbutton" label="Pop Me Up" popup="backpopup"/> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml b/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml new file mode 100644 index 0000000000..5927bec925 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_listcontrols.xhtml @@ -0,0 +1,152 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible focus event testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + let PromEvents = {}; + Services.scriptloader.loadSubScript( + "chrome://mochitests/content/a11y/accessible/tests/mochitest/promisified-events.js", + PromEvents); + //gA11yEventDumpID = "eventdump"; // debug stuff + gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + async function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + // Later tests use await. + let queueFinished = new Promise(resolve => { + gQueue.onFinish = function() { + resolve(); + return DO_NOT_FINISH_TEST; + }; + }); + + gQueue.push(new synthFocus("richlistbox", new focusChecker("rlb_item1"))); + gQueue.push(new synthDownKey("rlb_item1", new focusChecker("rlb_item2"))); + gQueue.push(new synthFocus("multiselrichlistbox", new focusChecker("msrlb_item1"))); + gQueue.push(new synthDownKey("msrlb_item1", new focusChecker("msrlb_item2"), { shiftKey: true })); + gQueue.push(new synthFocus("emptyrichlistbox", new focusChecker("emptyrichlistbox"))); + + gQueue.push(new synthFocus("menulist")); + gQueue.push(new synthClick("menulist", new focusChecker("ml_tangerine"))); + gQueue.push(new synthDownKey("ml_tangerine", new focusChecker("ml_marmalade"))); + gQueue.push(new synthEscapeKey("ml_marmalade", new focusChecker("menulist"))); + + // On Windows, items get selected during navigation. + let expectedItem = WIN ? "ml_strawberry" : "ml_marmalade"; + gQueue.push(new synthDownKey("menulist", new nofocusChecker(expectedItem))); + gQueue.push(new synthOpenComboboxKey("menulist", new focusChecker(expectedItem))); + gQueue.push(new synthEnterKey(expectedItem, new focusChecker("menulist"))); + + // no focus events for unfocused list controls when current item is + // changed. + gQueue.push(new synthFocus("emptyrichlistbox")); + + gQueue.push(new changeCurrentItem("richlistbox", "rlb_item1")); + gQueue.push(new changeCurrentItem("menulist", WIN ? "ml_marmalade" : "ml_tangerine")); + + gQueue.invoke(); + await queueFinished; + // Tests beyond this point use await rather than eventQueue. + + // When a menulist contains something other than XUL menuitems, we need + // to manage focus with aria-activedescendant. + info("Testing opening a menupopup with aria-activedescendant"); + let popupDiv1 = getNode("menupopup_ad_div1"); + let focused = PromEvents.waitForEvent(EVENT_FOCUS, popupDiv1); + let popup = getNode("menupopup_ad"); + popup.openPopup(); + await focused; + info("Testing removal of previous active descendant + setting new active descendant"); + focused = PromEvents.waitForEvent(EVENT_FOCUS, "menupopup_ad_div2"); + popupDiv1.remove(); + popup.setAttribute("aria-activedescendant", "menupopup_ad_div2"); + await focused; + popup.hidePopup(); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=433418" + title="Accessibles for focused HTML Select elements are not getting focused state"> + Mozilla Bug 433418 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=474893" + title="List controls should fire a focus event on the selected child when tabbing or when the selected child changes while the list is focused"> + Mozilla Bug 474893 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=552368" + title=" fire focus event on document accessible whenever the root or body element is focused"> + Mozilla Bug 552368 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <richlistbox id="richlistbox"> + <richlistitem id="rlb_item1"> + <description>A XUL Description!</description> + </richlistitem> + <richlistitem id="rlb_item2"> + <button label="A XUL Button"/> + </richlistitem> + </richlistbox> + <richlistbox id="multiselrichlistbox" seltype="multiple"> + <richlistitem id="msrlb_item1"> + <description>A XUL Description!</description> + </richlistitem> + <richlistitem id="msrlb_item2"> + <button label="A XUL Button"/> + </richlistitem> + </richlistbox> + <richlistbox id="emptyrichlistbox" seltype="multiple"/> + + <menulist id="menulist"> + <menupopup> + <menuitem id="ml_tangerine" label="tangerine trees"/> + <menuitem id="ml_marmalade" label="marmalade skies"/> + <menuitem id="ml_strawberry" label="strawberry fields"/> + </menupopup> + </menulist> + + <menulist> + <menupopup id="menupopup_ad" aria-activedescendant="menupopup_ad_div1"> + <div id="menupopup_ad_div1" role="option"></div> + <div id="menupopup_ad_div2" role="option"></div> + </menupopup> + </menulist> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_menu.xhtml b/accessible/tests/mochitest/events/test_focus_menu.xhtml new file mode 100644 index 0000000000..f4aee97221 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_menu.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Menu focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + + if (WIN) { + gQueue.push(new toggleTopMenu("fruit", new focusChecker("fruit"))); + gQueue.push(new synthRightKey("fruit", new focusChecker("vehicle"))); + gQueue.push(new synthEscapeKey("vehicle", new focusChecker(document))); + } + + // mouse move activate items but no focus event until menubar is active + gQueue.push(new synthMouseMove("fruit", new nofocusChecker("apple"))); + + // mouseover and click on menuitem makes it active before menubar is + // active + gQueue.push(new synthClick("fruit", new focusChecker("fruit"))); + + // mouseover on menuitem when menubar is active + gQueue.push(new synthMouseMove("apple", new focusChecker("apple"))); + + // keydown on disabled menuitem (disabled items are skipped on linux) + if (WIN) + gQueue.push(new synthDownKey("apple", new focusChecker("orange"))); + + // menu and menuitem are both active + // XXX: intermitent failure because two focus events may be coalesced, + // think to workaround or fix this issue, when done enable queue invoker + // below and remove next two. + //gQueue.push(new synthRightKey("apple", + // [ new focusChecker("vehicle"), + // new focusChecker("cycle")])); + gQueue.push(new synthClick("vehicle", new focusChecker("vehicle"))); + gQueue.push(new synthDownKey("cycle", new focusChecker("cycle"))); + + // open submenu + gQueue.push(new synthRightKey("cycle", new focusChecker("tricycle"))); + + // move to first menu in cycle, DOMMenuItemActive is fired for fruit, + // cycle and apple menuitems (bug 685191) + todo(false, "focus is fired for 'cycle' menuitem"); + //gQueue.push(new synthRightKey("vehicle", new focusChecker("apple"))); + + // click menuitem to close menu, focus gets back to document + gQueue.push(new synthClick("tricycle", new focusChecker(document))); + + //enableLogging("focus,DOMEvents,tree"); // logging for bug708927 + //gQueue.onFinish = function() { disableLogging(); } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673958" + title="Rework accessible focus handling"> + Mozilla Bug 673958 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menubar> + <menu id="fruit" label="Fruit"> + <menupopup> + <menuitem id="apple" label="Apple"/> + <menuitem id="orange" label="Orange" disabled="true"/> + </menupopup> + </menu> + <menu id="vehicle" label="Vehicle"> + <menupopup> + <menu id="cycle" label="cycle"> + <menupopup> + <menuitem id="tricycle" label="tricycle"/> + </menupopup> + </menu> + <menuitem id="car" label="Car" disabled="true"/> + </menupopup> + </menu> + </menubar> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_name.html b/accessible/tests/mochitest/events/test_focus_name.html new file mode 100644 index 0000000000..aa77923909 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_name.html @@ -0,0 +1,116 @@ +<html> + +<head> + <title>Accessible name testing on focus</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Checker for invokers. + */ + function actionChecker(aID, aDescription) { + this.__proto__ = new invokerChecker(EVENT_FOCUS, aID); + + this.check = function actionChecker_check(aEvent) { + var target = aEvent.accessible; + is(target.description, aDescription, + "Wrong description for " + prettyName(target)); + }; + } + + var gFocusHandler = { + handleEvent: function gFocusHandler_handleEvent(aEvent) { + var elm = aEvent.target; + if (elm.nodeType != Node.ELEMENT_NODE) + return; + + gTooltipElm.style.display = "block"; + + elm.setAttribute("aria-describedby", "tooltip"); + }, + }; + + var gBlurHandler = { + handleEvent: function gBlurHandler_handleEvent(aEvent) { + gTooltipElm.style.display = "none"; + + var elm = aEvent.target; + if (elm.nodeType == Node.ELEMENT_NODE) + elm.removeAttribute("aria-describedby"); + }, + }; + + /** + * Do tests. + */ + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + var gButtonElm = null; + var gTextboxElm = null; + var gTooltipElm = null; + + function doTests() { + gButtonElm = getNode("button"); + gTextboxElm = getNode("textbox"); + gTooltipElm = getNode("tooltip"); + + gButtonElm.addEventListener("focus", gFocusHandler); + gButtonElm.addEventListener("blur", gBlurHandler); + gTextboxElm.addEventListener("focus", gFocusHandler); + gTextboxElm.addEventListener("blur", gBlurHandler); + + // The aria-describedby is changed on DOM focus. Accessible description + // should be updated when a11y focus is fired. + gQueue = new eventQueue(nsIAccessibleEvent.EVENT_FOCUS); + gQueue.onFinish = function() { + gButtonElm.removeEventListener("focus", gFocusHandler); + gButtonElm.removeEventListener("blur", gBlurHandler); + gTextboxElm.removeEventListener("focus", gFocusHandler); + gTextboxElm.removeEventListener("blur", gBlurHandler); + }; + + var descr = "It's a tooltip"; + gQueue.push(new synthFocus("button", new actionChecker("button", descr))); + gQueue.push(new synthTab("textbox", new actionChecker("textbox", descr))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=520709" + title="mochitest to ensure name/description are updated on a11y focus if they were changed on DOM focus"> + Mozilla Bug 520709 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="tooltip" style="display: none" aria-hidden="true">It's a tooltip</div> + <button id="button">button</button> + <input id="textbox"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_removal.html b/accessible/tests/mochitest/events/test_focus_removal.html new file mode 100644 index 0000000000..eb47b07075 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_removal.html @@ -0,0 +1,95 @@ +<html> + +<head> + <title>Test removal of focused accessible</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function setFocus(aNodeToFocus, aExpectedFocus) { + let expected = aExpectedFocus || aNodeToFocus; + let focused = waitForEvent(EVENT_FOCUS, expected); + info("Focusing " + aNodeToFocus.id); + aNodeToFocus.focus(); + await focused; + ok(true, expected.id + " focused after " + + aNodeToFocus.id + ".focus()"); + } + + async function expectFocusAfterRemove(aNodeToRemove, aExpectedFocus, aDisplayNone = false) { + let focused = waitForEvent(EVENT_FOCUS, aExpectedFocus); + info("Removing " + aNodeToRemove.id); + if (aDisplayNone) { + aNodeToRemove.style.display = "none"; + } else { + aNodeToRemove.remove(); + } + await focused; + let friendlyExpected = aExpectedFocus == document ? + "document" : aExpectedFocus.id; + ok(true, friendlyExpected + " focused after " + + aNodeToRemove.id + " removed"); + } + + async function doTests() { + info("Testing removal of focused node itself"); + let button = getNode("button"); + await setFocus(button); + await expectFocusAfterRemove(button, document); + + info("Testing removal of focused node's parent"); + let dialog = getNode("dialog"); + let dialogButton = getNode("dialogButton"); + await setFocus(dialogButton); + await expectFocusAfterRemove(dialog, document); + + info("Testing removal of aria-activedescendant target"); + let listbox = getNode("listbox"); + let option = getNode("option"); + await setFocus(listbox, option); + await expectFocusAfterRemove(option, listbox); + + info("Test hiding focused element with display: none"); + let groupingButton = getNode("groupingButton"); + await setFocus(groupingButton); + await expectFocusAfterRemove(groupingButton, document, true); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <button id="button"></button> + + <div role="dialog" id="dialog"> + <button id="dialogButton"></button> + </div> + + <div role="listbox" id="listbox" tabindex="0" aria-activedescendant="option"> + <div role="option" id="option"></div> + </div> + + <div role="grouping" id="grouping"> + <button id="groupingButton"> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_selects.html b/accessible/tests/mochitest/events/test_focus_selects.html new file mode 100644 index 0000000000..ad9c3dc207 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_selects.html @@ -0,0 +1,115 @@ +<html> + +<head> + <title>Accessible focus testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTests() { + // Bug 746534 - File causes crash or hang on OS X + if (MAC) { + todo(false, "Bug 746534 - test file causes crash or hang on OS X"); + SimpleTest.finish(); + return; + } + + gQueue = new eventQueue(); + + // first item is focused until there's selection + gQueue.push(new synthFocus("list", new focusChecker("orange"))); + + // item is selected and stays focused + gQueue.push(new synthDownKey("list", new nofocusChecker())); + + // last selected item is focused + gQueue.push(new synthDownKey("list", new focusChecker("apple"), { shiftKey: true })); + + // no focus event if nothing is changed + gQueue.push(new synthDownKey("list", new nofocusChecker("apple"))); + + // current item is focused + gQueue.push(new synthUpKey("list", new focusChecker("orange"), { ctrlKey: true })); + + // focus on empty list (no items to be focused) + gQueue.push(new synthTab("list", new focusChecker("emptylist"))); + + // current item is focused + gQueue.push(new synthShiftTab("emptylist", new focusChecker("orange"))); + + // collapsed combobox keeps a focus + gQueue.push(new synthFocus("combobox", new focusChecker("combobox"))); + gQueue.push(new synthDownKey("combobox", new nofocusChecker("combobox"))); + + // selected item is focused for expanded combobox + gQueue.push(new synthOpenComboboxKey("combobox", new focusChecker("cb_apple"))); + gQueue.push(new synthUpKey("combobox", new focusChecker("cb_orange"))); + + // collapsed combobx keeps a focus + gQueue.push(new synthEscapeKey("combobox", new focusChecker("combobox"))); + + // no focus events for unfocused list controls when current item is + // changed + gQueue.push(new synthFocus("emptylist")); + + gQueue.push(new changeCurrentItem("list", "orange")); + gQueue.push(new changeCurrentItem("combobox", "cb_apple")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=433418" + title="Accessibles for focused HTML Select elements are not getting focused state"> + Mozilla Bug 433418 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=474893" + title="List controls should fire a focus event on the selected child when tabbing or when the selected child changes while the list is focused"> + Mozilla Bug 474893 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="list" size="5" multiple=""> + <option id="orange">Orange</option> + <option id="apple">Apple</option> + </select> + + <select id="emptylist" size="5"></select> + + <select id="combobox"> + <option id="cb_orange">Orange</option> + <option id="cb_apple">Apple</option> + </select> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_focus_tabbox.xhtml b/accessible/tests/mochitest/events/test_focus_tabbox.xhtml new file mode 100644 index 0000000000..1b808831dc --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_tabbox.xhtml @@ -0,0 +1,102 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Tabbox focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + //gA11yEventDumpID = "eventdump"; // debug stuff + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTests() + { + if (MAC) { + todo(false, "Tests disabled because of imminent failure."); + SimpleTest.finish(); + return; + } + + // Test focus events. + gQueue = new eventQueue(); + + var input = getNode("input"); + gQueue.push(new synthClick("tab1", new focusChecker("tab1"))); + gQueue.push(new synthTab("tab1", new focusChecker("checkbox1"))); + gQueue.push(new synthKey("tab1", "VK_TAB", { ctrlKey: true }, + new focusChecker(input))); + gQueue.push(new synthKey("tab2", "VK_TAB", { ctrlKey: true }, + new focusChecker("tab3"))); + gQueue.push(new synthKey("tab3", "VK_TAB", { ctrlKey: true }, + new focusChecker("tab1"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=370396" + title="Control+Tab to an empty tab panel in a tabbox causes focus to leave the tabbox"> + Mozilla Bug 370396 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tabbox> + <tabs> + <tab id="tab1" label="Tab1" selected="true"/> + <tab id="tab2" label="Tab2" /> + <tab id="tab3" label="Tab3" /> + </tabs> + <tabpanels> + <tabpanel orient="vertical"> + <groupbox orient="vertical"> + <checkbox id="checkbox1" label="Monday" width="75"/> + <checkbox label="Tuesday" width="75"/> + <checkbox label="Wednesday" width="75"/> + <checkbox label="Thursday" width="75"/> + <checkbox label="Friday" width="75"/> + <checkbox label="Saturday" width="75"/> + <checkbox label="Sunday" width="75"/> + </groupbox> + + <spacer style="height: 10px" /> + <label value="Label After checkboxes" /> + </tabpanel> + <tabpanel orient="vertical"> + <html:input id="input" /> + </tabpanel> + <tabpanel orient="vertical"> + <description>Tab 3 content</description> + </tabpanel> + </tabpanels> + </tabbox> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_focus_tree.xhtml b/accessible/tests/mochitest/events/test_focus_tree.xhtml new file mode 100644 index 0000000000..f36816c788 --- /dev/null +++ b/accessible/tests/mochitest/events/test_focus_tree.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL tree focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function focusTree(aTreeID) + { + var checker = new focusChecker(getFirstTreeItem, aTreeID); + this.__proto__ = new synthFocus(aTreeID, [ checker ]); + } + + function moveToNextItem(aTreeID) + { + var checker = new focusChecker(getSecondTreeItem, aTreeID); + this.__proto__ = new synthDownKey(aTreeID, [ checker ]); + } + + //////////////////////////////////////////////////////////////////////////// + // Helpers + + function getTreeItemAt(aTreeID, aIdx) + { return getAccessible(aTreeID).getChildAt(aIdx + 1); } + + function getFirstTreeItem(aTreeID) + { return getTreeItemAt(aTreeID, 0); } + + function getSecondTreeItem(aTreeID) + { return getTreeItemAt(aTreeID, 1); } + + //////////////////////////////////////////////////////////////////////////// + // Test + + var gQueue = null; + + //gA11yEventDumpID = "debug"; // debugging + //gA11yEventDumpToConsole = true; // debugging + + function doTest() + { + gQueue = new eventQueue(); + + gQueue.push(new focusTree("tree")); + gQueue.push(new moveToNextItem("tree")); + gQueue.push(new synthFocus("emptytree")); + + // no focus event for changed current item for unfocused tree + gQueue.push(new changeCurrentItem("tree", 0)); + + gQueue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(5)); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=386821" + title="Need better solution for firing delayed event against xul tree"> + Mozilla Bug 386821 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=406308" + title="Don't fire accessible focus events if widget is not actually in focus, confuses screen readers"> + Mozilla Bug 406308 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="debug"/> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col1" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + <tree id="emptytree" flex="1"> + <treecols> + <treecol id="emptytree_col1" flex="1" primary="true" label="column"/> + <treecol id="emptytree_col2" flex="1" label="column 2"/> + </treecols> + <treechildren id="emptytree_treechildren"/> + </tree> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/events/test_focusable_statechange.html b/accessible/tests/mochitest/events/test_focusable_statechange.html new file mode 100644 index 0000000000..8c841490bb --- /dev/null +++ b/accessible/tests/mochitest/events/test_focusable_statechange.html @@ -0,0 +1,96 @@ +<html> + +<head> + <title>Test removal of focused accessible</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + function focusableStateChange(id, enabled) { + return [EVENT_STATE_CHANGE, e => { + e.QueryInterface(nsIAccessibleStateChangeEvent); + return getAccessible(id) == e.accessible && + e.state == STATE_FOCUSABLE && (enabled == undefined || e.isEnabled == enabled); + }]; + } + + async function doTests() { + info("disable buttons."); + // Expect focusable change with 'disabled', + // and don't expect it with 'aria-disabled'. + let p = waitForEvents({ + expected: [focusableStateChange("button2", false)], + unexpected: [focusableStateChange("button1")] + }); + getNode("button1").setAttribute("aria-disabled", "true"); + getNode("button2").disabled = true; + await p; + + info("re-enable button"); + // Expect focusable change with 'disabled', + // and don't expect it with 'aria-disabled'. + p = waitForEvents({ + expected: [focusableStateChange("button2", true)], + unexpected: [focusableStateChange("button1")] + }); + getNode("button1").setAttribute("aria-disabled", "false"); + getNode("button2").disabled = false; + await p; + + info("add tabindex"); + // Expect focusable change on non-input, + // and don't expect event on an already focusable input. + p = waitForEvents({ + expected: [focusableStateChange("div", true)], + unexpected: [focusableStateChange("button2")] + }); + getNode("button2").tabIndex = "0"; + getNode("div").tabIndex = "0"; + await p; + + info("remove tabindex"); + // Expect focusable change when removing tabindex. + p = waitForEvent(...focusableStateChange("div", false)); + getNode("div").removeAttribute("tabindex"); + await p; + + p = waitForEvent(...focusableStateChange("link", false)); + getNode("link").removeAttribute("href"); + await p; + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <button id="button1"></button> + <button id="button2"></button> + + <div id="div">Hello</div> + + <a id="link" href="#">A link</a> + +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_fromUserInput.html b/accessible/tests/mochitest/events/test_fromUserInput.html new file mode 100644 index 0000000000..b3617358cf --- /dev/null +++ b/accessible/tests/mochitest/events/test_fromUserInput.html @@ -0,0 +1,112 @@ +<html> + +<head> + <title>Testing of isFromUserInput in text events</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + /** + * Remove text data from HTML input. + */ + function removeTextFromInput(aID, aStart, aEnd, aText, aFromUser) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new textChangeChecker(aID, aStart, aEnd, aText, false, aFromUser), + ]; + + this.invoke = function removeTextFromInput_invoke() { + this.DOMNode.focus(); + this.DOMNode.setSelectionRange(aStart, aEnd); + + synthesizeKey("KEY_Delete"); + }; + + this.getID = function removeTextFromInput_getID() { + return "Remove text from " + aStart + " to " + aEnd + " for " + + prettyName(aID); + }; + } + + /** + * Remove text data from text node. + */ + function removeTextFromContentEditable(aID, aStart, aEnd, aText, aFromUser) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new textChangeChecker(aID, aStart, aEnd, aText, false, aFromUser), + ]; + + this.invoke = function removeTextFromContentEditable_invoke() { + this.DOMNode.focus(); + this.textNode = getNode(aID).firstChild; + var selection = window.getSelection(); + var range = document.createRange(); + range.setStart(this.textNode, aStart); + range.setEnd(this.textNode, aEnd); + selection.addRange(range); + + synthesizeKey("KEY_Delete"); + }; + + this.getID = function removeTextFromContentEditable_getID() { + return "Remove text from " + aStart + " to " + aEnd + " for " + + prettyName(aID); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + // gA11yEventDumpID = "eventdump"; // debug stuff + + var gQueue = null; + + function doTests() { + gQueue = new eventQueue(); + + // Focused editable text node + gQueue.push(new removeTextFromContentEditable("div", 0, 3, "hel", true)); + + // Focused editable HTML input + gQueue.push(new removeTextFromInput("input", 1, 2, "n", true)); + + gQueue.invoke(); // Will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + + </script> +</head> + + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=686909" + title="isFromUserInput flag on accessible text change events not correct"> + Mozilla Bug 686909 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + <div id="eventdump"></div> + + <div id="div" contentEditable="true">hello</div> + <input id="input" value="input"> + +</body> + +</html> diff --git a/accessible/tests/mochitest/events/test_label.xhtml b/accessible/tests/mochitest/events/test_label.xhtml new file mode 100644 index 0000000000..d9bb7baa45 --- /dev/null +++ b/accessible/tests/mochitest/events/test_label.xhtml @@ -0,0 +1,176 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Tests: accessible XUL label/description events"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Invokers + + const kRecreated = 0; + const kTextRemoved = 1; + const kTextChanged = 2; + + const kNoValue = 0; + + /** + * Set/remove @value attribute. + */ + function setValue(aID, aValue, aResult, aOldValue) + { + this.labelNode = getNode(aID); + + this.eventSeq = []; + + switch (aResult) { + case kRecreated: + this.eventSeq.push(new invokerChecker(EVENT_HIDE, this.labelNode)); + this.eventSeq.push(new invokerChecker(EVENT_SHOW, this.labelNode)); + break; + case kTextRemoved: + this.eventSeq.push( + new textChangeChecker(this.labelNode, 0, aOldValue.length, + aOldValue, false)); + break; + case kTextChanged: + this.eventSeq.push( + new textChangeChecker(this.labelNode, 0, aOldValue.length, + aOldValue, false)); + this.eventSeq.push( + new textChangeChecker(this.labelNode, 0, aValue.length, + aValue, true)); + break; + } + + this.invoke = function setValue_invoke() + { + if (aValue === kNoValue) + this.labelNode.removeAttribute("value"); + else + this.labelNode.setAttribute("value", aValue); + } + + this.finalCheck = function setValue_finalCheck() + { + var tree = + { LABEL: [ + { TEXT_LEAF: [ ] } + ] }; + testAccessibleTree(aID, tree); + } + + this.getID = function setValue_getID() + { + return "set @value='" + aValue + "' for label " + prettyName(aID); + } + } + + /** + * Change @crop attribute. + */ + function setCrop(aID, aCropValue, aRemovedText, aInsertedText) + { + this.labelNode = getNode(aID); + this.width = this.labelNode.getBoundingClientRect().width; + this.charWidth = this.width / this.labelNode.value.length; + + this.eventSeq = [ + new textChangeChecker(this.labelNode, 0, -1, aRemovedText, false), + new textChangeChecker(this.labelNode, 0, -1, aInsertedText, true) + ]; + + this.invoke = function setCrop_invoke() + { + if (!this.labelNode.hasAttribute("crop")) + this.labelNode.width = Math.floor(this.width - 2 * this.charWidth); + + this.labelNode.setAttribute("crop", aCropValue); + } + + this.getID = function setCrop_finalCheck() + { + return "set crop " + aCropValue; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(); + + gQueue.push(new setValue("label", "shiroka strana", kRecreated)); + gQueue.push(new setValue("label", "?<>!+_", kTextChanged, "shiroka strana")); + gQueue.push(new setValue("label", "", kTextRemoved, "?<>!+_")); + gQueue.push(new setValue("label", kNoValue, kRecreated)); + + gQueue.push(new setValue("descr", "hello world", kRecreated)); + gQueue.push(new setValue("descr", "si_ya", kTextChanged, "hello world")); + gQueue.push(new setValue("descr", "", kTextRemoved, "si_ya")); + gQueue.push(new setValue("descr", kNoValue, kRecreated)); + + if (MAC) { + // "valuetocro" -> "…etocro" + gQueue.push(new setCrop("croplabel", "left", "valu", "…")); + // "…etocro", "val…cro" + gQueue.push(new setCrop("croplabel", "center", "…eto", "val…")); + } else { + // "valuetocro" -> "…uetocro" + gQueue.push(new setCrop("croplabel", "left", "val", "…")); + // "…uetocro" -> "valu…cro" + gQueue.push(new setCrop("croplabel", "center", "…ueto", "valu…")); + } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=396166" + title="xul:label@value accessible should implement nsIAccessibleText"> + Bug 396166 + </a> + <br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <label id="label">hello</label> + <description id="descr">hello</description> + + <hbox> + <label id="croplabel" value="valuetocro" + style="font-family: monospace;"/> + </hbox> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/events/test_menu.xhtml b/accessible/tests/mochitest/events/test_menu.xhtml new file mode 100644 index 0000000000..e089c76d0b --- /dev/null +++ b/accessible/tests/mochitest/events/test_menu.xhtml @@ -0,0 +1,200 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible menu events testing for XUL menu"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + function openFileMenu() + { + this.eventSeq = [ + new invokerChecker(EVENT_MENU_START, getNode("menubar")), + new invokerChecker(EVENT_MENUPOPUP_START, getNode("menupopup-file")) + // new invokerChecker(EVENT_FOCUS, getNode("menuitem-newtab")) intermitent failure + ]; + + this.invoke = function openFileMenu_invoke() + { + synthesizeKey("F", {altKey: true, shiftKey: true}); + } + + this.getID = function openFileMenu_getID() + { + return "open file menu by alt+F press"; + } + } + + function openEditMenu() + { + this.eventSeq = [ + new invokerChecker(EVENT_MENUPOPUP_END, getNode("menupopup-file")), + new invokerChecker(EVENT_MENUPOPUP_START, getNode("menupopup-edit")) + // new invokerChecker(EVENT_FOCUS, getNode("menuitem-undo")) intermitent failure + ]; + + this.invoke = function openEditMenu_invoke() + { + synthesizeKey("KEY_ArrowRight"); + } + + this.getID = function openEditMenu_getID() + { + return "open edit menu by lef arrow press"; + } + } + + function closeEditMenu() + { + this.eventSeq = [ + //new invokerChecker(EVENT_FOCUS, document), intermitent failure + new invokerChecker(EVENT_MENUPOPUP_END, getNode("menupopup-edit")), + new invokerChecker(EVENT_MENU_END, getNode("menubar")) + ]; + + this.invoke = function closeEditMenu_invoke() + { + synthesizeKey("KEY_Escape"); + } + + this.getID = function closeEditMenu_getID() + { + return "close edit menu, leave menubar"; + } + } + + function focusFileMenu() + { + this.eventSeq = [ + new invokerChecker(EVENT_MENU_START, getNode("menubar")) + // new invokerChecker(EVENT_FOCUS, getNode("menuitem-file")) //intermitent failure + ]; + + this.invoke = function focusFileMenu_invoke() + { + synthesizeKey("KEY_Alt"); + } + + this.getID = function focusFileMenu_getID() + { + return "activate menubar, focus file menu (atl press)"; + } + } + + function focusEditMenu() + { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getNode("menuitem-edit")) + ]; + + this.invoke = function focusEditMenu_invoke() + { + synthesizeKey("KEY_ArrowRight"); + } + + this.getID = function focusEditMenu_getID() + { + return "focus edit menu"; + } + } + + function leaveMenubar() + { + this.eventSeq = [ + //new invokerChecker(EVENT_FOCUS, document), intermitent failure + new invokerChecker(EVENT_MENU_END, getNode("menubar")) + ]; + + this.invoke = function leaveMenubar_invoke() + { + synthesizeKey("KEY_Escape"); + } + + this.getID = function leaveMenubar_getID() + { + return "leave menubar"; + } + } + + /** + * Do tests. + */ + + //gA11yEventDumpID = "eventdump"; + //gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTests() + { + if (!WIN) { + todo(false, "Enable this test on other platforms."); + SimpleTest.finish(); + return; + } + + todo(false, + "Fix intermitent failures. Focus may randomly occur before or after menupopup events!"); + + gQueue = new eventQueue(); + + gQueue.push(new openFileMenu()); + gQueue.push(new openEditMenu()); + gQueue.push(new closeEditMenu()); + + // Alt key is used to active menubar and focus menu item on Windows, + // other platforms requires setting a ui.key.menuAccessKeyFocuses + // preference. + if (WIN || LINUX) { + gQueue.push(new focusFileMenu()); + gQueue.push(new focusEditMenu()); + gQueue.push(new leaveMenubar()); + } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=615189" + title="Clean up FireAccessibleFocusEvent"> + Mozilla Bug 615189 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menubar id="menubar"> + <menu id="menuitem-file" label="File" accesskey="F"> + <menupopup id="menupopup-file"> + <menuitem id="menuitem-newtab" label="New Tab"/> + </menupopup> + </menu> + <menu id="menuitem-edit" label="Edit" accesskey="E"> + <menupopup id="menupopup-edit"> + <menuitem id="menuitem-undo" label="Undo"/> + </menupopup> + </menu> + </menubar> + + <vbox id="eventdump" role="log"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_mutation.html b/accessible/tests/mochitest/events/test_mutation.html new file mode 100644 index 0000000000..7ee876570b --- /dev/null +++ b/accessible/tests/mochitest/events/test_mutation.html @@ -0,0 +1,580 @@ +<html> + +<head> + <title>Accessible mutation events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style> + div.displayNone a { display:none; } + div.visibilityHidden a { visibility:hidden; } +</style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Invokers. + */ + var kNoEvents = 0; + + var kShowEvent = 1; + var kHideEvent = 2; + var kReorderEvent = 4; + var kShowEvents = kShowEvent | kReorderEvent; + var kHideEvents = kHideEvent | kReorderEvent; + var kHideAndShowEvents = kHideEvents | kShowEvent; + + /** + * Base class to test mutation a11y events. + * + * @param aNodeOrID [in] node invoker's action is executed for + * @param aEventTypes [in] events to register (see constants above) + * @param aDoNotExpectEvents [in] boolean indicates if events are expected + */ + function mutateA11yTree(aNodeOrID, aEventTypes, aDoNotExpectEvents) { + // Interface + this.DOMNode = getNode(aNodeOrID); + this.doNotExpectEvents = aDoNotExpectEvents; + this.eventSeq = []; + this.unexpectedEventSeq = []; + + /** + * Change default target (aNodeOrID) registered for the given event type. + */ + this.setTarget = function mutateA11yTree_setTarget(aEventType, aTarget) { + var type = this.getA11yEventType(aEventType); + for (var idx = 0; idx < this.getEventSeq().length; idx++) { + if (this.getEventSeq()[idx].type == type) { + this.getEventSeq()[idx].target = aTarget; + return idx; + } + } + return -1; + }; + + /** + * Replace the default target currently registered for a given event type + * with the nodes in the targets array. + */ + this.setTargets = function mutateA11yTree_setTargets(aEventType, aTargets) { + var targetIdx = this.setTarget(aEventType, aTargets[0]); + + var type = this.getA11yEventType(aEventType); + for (var i = 1; i < aTargets.length; i++) { + let checker = new invokerChecker(type, aTargets[i]); + this.getEventSeq().splice(++targetIdx, 0, checker); + } + }; + + // Implementation + this.getA11yEventType = function mutateA11yTree_getA11yEventType(aEventType) { + if (aEventType == kReorderEvent) + return nsIAccessibleEvent.EVENT_REORDER; + + if (aEventType == kHideEvent) + return nsIAccessibleEvent.EVENT_HIDE; + + if (aEventType == kShowEvent) + return nsIAccessibleEvent.EVENT_SHOW; + + return 0; + }; + + this.getEventSeq = function mutateA11yTree_getEventSeq() { + return this.doNotExpectEvents ? this.unexpectedEventSeq : this.eventSeq; + }; + + if (aEventTypes & kHideEvent) { + let checker = new invokerChecker(this.getA11yEventType(kHideEvent), + this.DOMNode); + this.getEventSeq().push(checker); + } + + if (aEventTypes & kShowEvent) { + let checker = new invokerChecker(this.getA11yEventType(kShowEvent), + this.DOMNode); + this.getEventSeq().push(checker); + } + + if (aEventTypes & kReorderEvent) { + let checker = new invokerChecker(this.getA11yEventType(kReorderEvent), + this.DOMNode.parentNode); + this.getEventSeq().push(checker); + } + } + + /** + * Change CSS style for the given node. + */ + function changeStyle(aNodeOrID, aProp, aValue, aEventTypes) { + this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false); + + this.invoke = function changeStyle_invoke() { + this.DOMNode.style[aProp] = aValue; + }; + + this.getID = function changeStyle_getID() { + return aNodeOrID + " change style " + aProp + " on value " + aValue; + }; + } + + /** + * Change class name for the given node. + */ + function changeClass(aParentNodeOrID, aNodeOrID, aClassName, aEventTypes) { + this.__proto__ = new mutateA11yTree(aNodeOrID, aEventTypes, false); + + this.invoke = function changeClass_invoke() { + this.parentDOMNode.className = aClassName; + }; + + this.getID = function changeClass_getID() { + return aNodeOrID + " change class " + aClassName; + }; + + this.parentDOMNode = getNode(aParentNodeOrID); + } + + /** + * Clone the node and append it to its parent. + */ + function cloneAndAppendToDOM(aNodeOrID, aEventTypes, + aTargetsFunc, aReorderTargetFunc) { + var eventTypes = aEventTypes || kShowEvents; + var doNotExpectEvents = (aEventTypes == kNoEvents); + + this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes, + doNotExpectEvents); + + this.invoke = function cloneAndAppendToDOM_invoke() { + var newElm = this.DOMNode.cloneNode(true); + newElm.removeAttribute("id"); + + var targets = aTargetsFunc ? + aTargetsFunc(newElm) : [newElm]; + this.setTargets(kShowEvent, targets); + + if (aReorderTargetFunc) { + var reorderTarget = aReorderTargetFunc(this.DOMNode); + this.setTarget(kReorderEvent, reorderTarget); + } + + this.DOMNode.parentNode.appendChild(newElm); + }; + + this.getID = function cloneAndAppendToDOM_getID() { + return aNodeOrID + " clone and append to DOM."; + }; + } + + /** + * Removes the node from DOM. + */ + function removeFromDOM(aNodeOrID, aEventTypes, + aTargetsFunc, aReorderTargetFunc) { + var eventTypes = aEventTypes || kHideEvents; + var doNotExpectEvents = (aEventTypes == kNoEvents); + + this.__proto__ = new mutateA11yTree(aNodeOrID, eventTypes, + doNotExpectEvents); + + this.invoke = function removeFromDOM_invoke() { + this.DOMNode.remove(); + }; + + this.getID = function removeFromDOM_getID() { + return prettyName(aNodeOrID) + " remove from DOM."; + }; + + if (aTargetsFunc && (eventTypes & kHideEvent)) + this.setTargets(kHideEvent, aTargetsFunc(this.DOMNode)); + + if (aReorderTargetFunc && (eventTypes & kReorderEvent)) + this.setTarget(kReorderEvent, aReorderTargetFunc(this.DOMNode)); + } + + /** + * Clone the node and replace the original node by cloned one. + */ + function cloneAndReplaceInDOM(aNodeOrID) { + this.__proto__ = new mutateA11yTree(aNodeOrID, kHideAndShowEvents, + false); + + this.invoke = function cloneAndReplaceInDOM_invoke() { + this.DOMNode.parentNode.replaceChild(this.newElm, this.DOMNode); + }; + + this.getID = function cloneAndReplaceInDOM_getID() { + return aNodeOrID + " clone and replace in DOM."; + }; + + this.newElm = this.DOMNode.cloneNode(true); + this.newElm.removeAttribute("id"); + this.setTarget(kShowEvent, this.newElm); + } + + /** + * Trigger content insertion (flush layout), removal and insertion of + * the same element for the same parent. + */ + function test1(aContainerID) { + this.divNode = document.createElement("div"); + this.divNode.setAttribute("id", "div-test1"); + this.containerNode = getNode(aContainerID); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.divNode), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function test1_invoke() { + this.containerNode.appendChild(this.divNode); + getComputedStyle(this.divNode, "").color; + this.containerNode.removeChild(this.divNode); + this.containerNode.appendChild(this.divNode); + }; + + this.getID = function test1_getID() { + return "fuzzy test #1: content insertion (flush layout), removal and" + + "reinsertion"; + }; + } + + /** + * Trigger content insertion (flush layout), removal and insertion of + * the same element for the different parents. + */ + function test2(aContainerID, aTmpContainerID) { + this.divNode = document.createElement("div"); + this.divNode.setAttribute("id", "div-test2"); + this.containerNode = getNode(aContainerID); + this.tmpContainerNode = getNode(aTmpContainerID); + this.container = getAccessible(this.containerNode); + this.tmpContainer = getAccessible(this.tmpContainerNode); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.divNode), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_REORDER, this.tmpContainerNode), + ]; + + this.invoke = function test2_invoke() { + this.tmpContainerNode.appendChild(this.divNode); + getComputedStyle(this.divNode, "").color; + this.tmpContainerNode.removeChild(this.divNode); + this.containerNode.appendChild(this.divNode); + }; + + this.getID = function test2_getID() { + return "fuzzy test #2: content insertion (flush layout), removal and" + + "reinsertion under another container"; + }; + } + + /** + * Content insertion (flush layout) and then removal (nothing was changed). + */ + function test3(aContainerID) { + this.divNode = document.createElement("div"); + this.divNode.setAttribute("id", "div-test3"); + this.containerNode = getNode(aContainerID); + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_SHOW, this.divNode), + new invokerChecker(EVENT_HIDE, this.divNode), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function test3_invoke() { + this.containerNode.appendChild(this.divNode); + getComputedStyle(this.divNode, "").color; + this.containerNode.removeChild(this.divNode); + }; + + this.getID = function test3_getID() { + return "fuzzy test #3: content insertion (flush layout) and removal"; + }; + } + + function insertReferredElm(aContainerID) { + this.containerNode = getNode(aContainerID); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.firstChild; }, this.containerNode), + new invokerChecker(EVENT_SHOW, function(aNode) { return aNode.lastChild; }, this.containerNode), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function insertReferredElm_invoke() { + let span = document.createElement("span"); + span.setAttribute("id", "insertReferredElms_span"); + let input = document.createElement("input"); + input.setAttribute("aria-labelledby", "insertReferredElms_span"); + this.containerNode.appendChild(span); + this.containerNode.appendChild(input); + }; + + this.getID = function insertReferredElm_getID() { + return "insert inaccessible element and then insert referring element to make it accessible"; + }; + } + + function showHiddenParentOfVisibleChild() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("c4_child")), + new invokerChecker(EVENT_SHOW, getNode("c4_middle")), + new invokerChecker(EVENT_REORDER, getNode("c4")), + ]; + + this.invoke = function showHiddenParentOfVisibleChild_invoke() { + getNode("c4_middle").style.visibility = "visible"; + }; + + this.getID = function showHiddenParentOfVisibleChild_getID() { + return "show hidden parent of visible child"; + }; + } + + function hideNDestroyDoc() { + this.txt = null; + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, () => { return this.txt; }), + ]; + + this.invoke = function hideNDestroyDoc_invoke() { + this.txt = getAccessible("c5").firstChild.firstChild; + this.txt.DOMNode.remove(); + }; + + this.check = function hideNDestroyDoc_check() { + getNode("c5").remove(); + }; + + this.getID = function hideNDestroyDoc_getID() { + return "remove text node and destroy a document on hide event"; + }; + } + + function hideHideNDestroyDoc() { + this.target = null; + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, () => { return this.target; }), + ]; + + this.invoke = function hideHideNDestroyDoc_invoke() { + var doc = getAccessible("c6").firstChild; + var l1 = doc.firstChild; + this.target = l1.firstChild; + var l2 = doc.lastChild; + l1.DOMNode.firstChild.remove(); + l2.DOMNode.firstChild.remove(); + }; + + this.check = function hideHideNDestroyDoc_check() { + getNode("c6").remove(); + }; + + this.getID = function hideHideNDestroyDoc_getID() { + return "remove text nodes (2 events in the queue) and destroy a document on first hide event"; + }; + } + + /** + * Target getters. + */ + function getFirstChild(aNode) { + return [aNode.firstChild]; + } + function getLastChild(aNode) { + return [aNode.lastChild]; + } + + function getNEnsureFirstChild(aNode) { + var node = aNode.firstChild; + getAccessible(node); + return [node]; + } + + function getNEnsureChildren(aNode) { + var children = []; + var node = aNode.firstChild; + do { + children.push(node); + getAccessible(node); + node = node.nextSibling; + } while (node); + + return children; + } + + function getParent(aNode) { + return aNode.parentNode; + } + + // gA11yEventDumpToConsole = true; // debug stuff + // enableLogging("events,verbose"); + + /** + * Do tests. + */ + var gQueue = null; + + function doTests() { + gQueue = new eventQueue(); + + // Show/hide events by changing of display style of accessible DOM node + // from 'inline' to 'none', 'none' to 'inline'. + let id = "link1"; + getAccessible(id); // ensure accessible is created + gQueue.push(new changeStyle(id, "display", "none", kHideEvents)); + gQueue.push(new changeStyle(id, "display", "inline", kShowEvents)); + + // Show/hide events by changing of visibility style of accessible DOM node + // from 'visible' to 'hidden', 'hidden' to 'visible'. + id = "link2"; + getAccessible(id); + gQueue.push(new changeStyle(id, "visibility", "hidden", kHideEvents)); + gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents)); + + // Show/hide events by changing of visibility style of accessible DOM node + // from 'collapse' to 'visible', 'visible' to 'collapse'. + id = "link4"; + gQueue.push(new changeStyle(id, "visibility", "visible", kShowEvents)); + gQueue.push(new changeStyle(id, "visibility", "collapse", kHideEvents)); + + // Show/hide events by adding new accessible DOM node and removing old one. + id = "link5"; + gQueue.push(new cloneAndAppendToDOM(id)); + gQueue.push(new removeFromDOM(id)); + + // No show/hide events by adding new not accessible DOM node and removing + // old one, no reorder event for their parent. + id = "child1"; + gQueue.push(new cloneAndAppendToDOM(id, kNoEvents)); + gQueue.push(new removeFromDOM(id, kNoEvents)); + + // Show/hide events by adding new accessible DOM node and removing + // old one, there is reorder event for their parent. + id = "child2"; + gQueue.push(new cloneAndAppendToDOM(id)); + gQueue.push(new removeFromDOM(id)); + + // Show/hide events by adding new DOM node containing accessible DOM and + // removing old one, there is reorder event for their parent. + id = "child3"; + gQueue.push(new cloneAndAppendToDOM(id, kShowEvents, getFirstChild, + getParent)); + + // Hide event for accessible child of unaccessible removed DOM node and + // reorder event for its parent. + gQueue.push(new removeFromDOM(id, kHideEvents, + getNEnsureFirstChild, getParent)); + + // Hide events for accessible children of unaccessible removed DOM node + // and reorder event for its parent. + gQueue.push(new removeFromDOM("child4", kHideEvents, + getNEnsureChildren, getParent)); + + // Show/hide events by creating new accessible DOM node and replacing + // old one. + getAccessible("link6"); // ensure accessible is created + gQueue.push(new cloneAndReplaceInDOM("link6")); + + // Show/hide events by changing class name on the parent node. + gQueue.push(new changeClass("container2", "link7", "", kShowEvents)); + gQueue.push(new changeClass("container2", "link7", "displayNone", + kHideEvents)); + + gQueue.push(new changeClass("container3", "link8", "", kShowEvents)); + gQueue.push(new changeClass("container3", "link8", "visibilityHidden", + kHideEvents)); + + gQueue.push(new test1("testContainer")); + gQueue.push(new test2("testContainer", "testContainer2")); + gQueue.push(new test2("testContainer", "testNestedContainer")); + gQueue.push(new test3("testContainer")); + gQueue.push(new insertReferredElm("testContainer3")); + gQueue.push(new showHiddenParentOfVisibleChild()); + + gQueue.push(new hideNDestroyDoc()); + gQueue.push(new hideHideNDestroyDoc()); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=469985" + title=" turn the test from bug 354745 into mochitest"> + Mozilla Bug 469985</a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=472662" + title="no reorder event when html:link display property is changed from 'none' to 'inline'"> + Mozilla Bug 472662</a> + <a target="_blank" + title="Rework accessible tree update code" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"> + Mozilla Bug 570275</a> + <a target="_blank" + title="Develop a way to handle visibility style" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=606125"> + Mozilla Bug 606125</a> + <a target="_blank" + title="Update accessible tree on content insertion after layout" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=498015"> + Mozilla Bug 498015</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <div id="testContainer"> + <a id="link1" href="http://www.google.com">Link #1</a> + <a id="link2" href="http://www.google.com">Link #2</a> + <a id="link3" href="http://www.google.com">Link #3</a> + <a id="link4" href="http://www.google.com" style="visibility:collapse">Link #4</a> + <a id="link5" href="http://www.google.com">Link #5</a> + + <div id="container" role="list"> + <span id="child1"></span> + <span id="child2" role="listitem"></span> + <span id="child3"><span role="listitem"></span></span> + <span id="child4"><span id="child4_1" role="listitem"></span><span id="child4_2" role="listitem"></span></span> + </div> + + <a id="link6" href="http://www.google.com">Link #6</a> + + <div id="container2" class="displayNone"><a id="link7">Link #7</a></div> + <div id="container3" class="visibilityHidden"><a id="link8">Link #8</a></div> + <div id="testNestedContainer"></div> + </div> + <div id="testContainer2"></div> + <div id="testContainer3"></div> + + <div id="c4"> + <div style="visibility:hidden" id="c4_middle"> + <div style="visibility:visible" id="c4_child"></div> + </div> + + <iframe id="c5" src="data:text/html,hey"></iframe> + <iframe id="c6" src="data:text/html,<label>l</label><label>l</label>"></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_namechange.html b/accessible/tests/mochitest/events/test_namechange.html new file mode 100644 index 0000000000..02d888a4ae --- /dev/null +++ b/accessible/tests/mochitest/events/test_namechange.html @@ -0,0 +1,119 @@ +<html> + +<head> + <title>Accessible name change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function setAttr(aID, aAttr, aValue, aChecker) { + this.eventSeq = [ aChecker ]; + this.invoke = function setAttr_invoke() { + getNode(aID).setAttribute(aAttr, aValue); + }; + + this.getID = function setAttr_getID() { + return "set attr '" + aAttr + "', value '" + aValue + "'"; + }; + } + + /** + * No name change on an accessible, because the accessible is recreated. + */ + function setAttr_recreate(aID, aAttr, aValue) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getAccessible(aID)), + new invokerChecker(EVENT_SHOW, aID), + ]; + this.invoke = function setAttr_recreate_invoke() { + todo(false, "No accessible recreation should happen, just name change event"); + getNode(aID).setAttribute(aAttr, aValue); + }; + + this.getID = function setAttr_recreate_getID() { + return "set attr '" + aAttr + "', value '" + aValue + "'"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuggin + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new setAttr("tst1", "aria-label", "hi", + new invokerChecker(EVENT_NAME_CHANGE, "tst1"))); + gQueue.push(new setAttr("tst1", "aria-labelledby", "display", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1"))); + gQueue.push(new setAttr("tst1", "alt", "alt", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1"))); + gQueue.push(new setAttr("tst1", "title", "title", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst1"))); + + gQueue.push(new setAttr("tst2", "aria-labelledby", "display", + new invokerChecker(EVENT_NAME_CHANGE, "tst2"))); + gQueue.push(new setAttr("tst2", "alt", "alt", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2"))); + gQueue.push(new setAttr("tst2", "title", "title", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst2"))); + + // When `alt` attribute is added or removed from a broken img, + // the accessible is recreated. + gQueue.push(new setAttr_recreate("tst3", "alt", "one")); + // When an `alt` attribute is changed, there is a name change event. + gQueue.push(new setAttr("tst3", "alt", "two", + new invokerChecker(EVENT_NAME_CHANGE, "tst3"))); + gQueue.push(new setAttr("tst3", "title", "title", + new unexpectedInvokerChecker(EVENT_NAME_CHANGE, "tst3"))); + + gQueue.push(new setAttr("tst4", "title", "title", + new invokerChecker(EVENT_NAME_CHANGE, "tst4"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=991969" + title="Event not fired when description changes"> + Bug 991969 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <img id="tst1" alt="initial" src="../moz.png"> + <img id="tst2" src="../moz.png"> + <img id="tst3"> + <img id="tst4" src="../moz.png"> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_namechange.xhtml b/accessible/tests/mochitest/events/test_namechange.xhtml new file mode 100644 index 0000000000..a6dd8cb218 --- /dev/null +++ b/accessible/tests/mochitest/events/test_namechange.xhtml @@ -0,0 +1,90 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/chrome-harness.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + + /** + * Check name changed a11y event. + */ + function nameChangeChecker(aMsg, aID) + { + this.type = EVENT_NAME_CHANGE; + + function targetGetter() + { + return getAccessible(aID); + } + Object.defineProperty(this, "target", { get: targetGetter }); + + this.getID = function getID() + { + return aMsg + " name changed"; + } + } + + function changeRichListItemChild() + { + this.invoke = function changeRichListItemChild_invoke() + { + getNode('childcontent').setAttribute('value', 'Changed.'); + } + + this.eventSeq = + [ + new nameChangeChecker("changeRichListItemChild: ", "listitem") + ]; + + this.getID = function changeRichListItemChild_getID() + { + return "changeRichListItemChild"; + } + } + + function doTest() + { + var queue = new eventQueue(); + queue.push(new changeRichListItemChild()); + queue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=986054" + title="Propagate name change events"> + Mozilla Bug 986054 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <richlistbox> + <richlistitem id="listitem"> + <description id="childcontent" value="This will be changed."/> + </richlistitem> + </richlistbox> + </vbox> +</window> diff --git a/accessible/tests/mochitest/events/test_scroll.xhtml b/accessible/tests/mochitest/events/test_scroll.xhtml new file mode 100644 index 0000000000..cf3aaa7cb4 --- /dev/null +++ b/accessible/tests/mochitest/events/test_scroll.xhtml @@ -0,0 +1,141 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/chrome-harness.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Tests + + function getAnchorJumpInTabDocument(aTabIdx) + { + var tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument(); + return tabDoc.querySelector("a[name='link1']"); + } + + function loadTab(aURL) + { + this.eventSeq = [ + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument), + new asyncInvokerChecker(EVENT_SCROLLING_START, getAnchorJumpInTabDocument) + ]; + + this.invoke = function loadTab_invoke() + { + tabBrowser().loadURI(aURL, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + // Flush layout, so as to guarantee that the a11y tree is constructed. + browserDocument().documentElement.getBoundingClientRect(); + } + + this.getID = function loadTab_getID() + { + return "load tab: " + aURL; + } + } + + function loadTabInBackground(aURL) + { + this.eventSeq = [ + new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 1) + ]; + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_SCROLLING_START, getAnchorJumpInTabDocument, 1) + ]; + + this.invoke = function loadTabInBackground_invoke() + { + tabBrowser().loadOneTab(aURL, { + referrerURI: null, + charset: "", + postData: null, + inBackground: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + // Flush layout, so as to guarantee that the a11y tree is constructed. + browserDocument().documentElement.getBoundingClientRect(); + } + + this.getID = function loadTabInBackground_getID() + { + return "load tab in background: " + aURL; + } + } + + function switchToBackgroundTab() + { + this.eventSeq = [ + new invokerChecker(EVENT_SCROLLING_START, getAnchorJumpInTabDocument) + ]; + + this.invoke = function switchToBackgroundTab_invoke() + { + tabBrowser().selectTabAtIndex(1); + } + + this.getID = function switchToBackgroundTab_getID() + { + return "switch to background tab"; + } + } + + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(); + + var url = "http://mochi.test:8888/a11y/accessible/tests/mochitest/events/scroll.html#link1"; + gQueue.push(new loadTab(url)); + gQueue.push(new loadTabInBackground(url)); + gQueue.push(new switchToBackgroundTab()); + gQueue.onFinish = function() { closeBrowserWindow(); } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTest); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=691734" + title="Make sure scrolling start event is fired when document receive focus"> + Mozilla Bug 691734 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="eventdump"></vbox> + </vbox> +</window> diff --git a/accessible/tests/mochitest/events/test_scroll_caret.xhtml b/accessible/tests/mochitest/events/test_scroll_caret.xhtml new file mode 100644 index 0000000000..e696b128bd --- /dev/null +++ b/accessible/tests/mochitest/events/test_scroll_caret.xhtml @@ -0,0 +1,91 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/chrome-harness.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Tests + + function getAnchorJumpInTabDocument(aTabIdx) + { + var tabDoc = aTabIdx ? tabDocumentAt(aTabIdx) : currentTabDocument(); + return tabDoc.querySelector("h1[id='heading_1']"); + } + + function loadTab(aURL) + { + this.eventSeq = [ + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument), + new asyncCaretMoveChecker(0, getAnchorJumpInTabDocument) + ]; + + this.invoke = function loadTab_invoke() + { + tabBrowser().loadURI(aURL, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + } + + this.getID = function loadTab_getID() + { + return "load tab: " + aURL; + } + } + + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(); + + var url = "http://mochi.test:8888/a11y/accessible/tests/mochitest/events/scroll.html#heading_1"; + gQueue.push(new loadTab(url)); + gQueue.onFinish = function() { closeBrowserWindow(); } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTest); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1056459" + title="Make sure caret move event is fired when document receive focus"> + Mozilla Bug 1056459 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="eventdump"></vbox> + </vbox> +</window> diff --git a/accessible/tests/mochitest/events/test_selection.html b/accessible/tests/mochitest/events/test_selection.html new file mode 100644 index 0000000000..a976d54b30 --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection.html @@ -0,0 +1,115 @@ +<html> + +<head> + <title>Accessible selection event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpToConsole = true; // debuggin + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + // open combobox + gQueue.push(new synthClick("combobox", + new invokerChecker(EVENT_FOCUS, "cb1_item1"))); + gQueue.push(new synthDownKey("cb1_item1", + selChangeSeq("cb1_item1", "cb1_item2"))); + + // closed combobox + gQueue.push(new synthEscapeKey("combobox", + new invokerChecker(EVENT_FOCUS, "combobox"))); + gQueue.push(new synthDownKey("cb1_item2", + selChangeSeq("cb1_item2", "cb1_item3"))); + + // listbox + gQueue.push(new synthClick("lb1_item1", + new invokerChecker(EVENT_SELECTION, "lb1_item1"))); + gQueue.push(new synthDownKey("lb1_item1", + selChangeSeq("lb1_item1", "lb1_item2"))); + + // multiselectable listbox + gQueue.push(new synthClick("lb2_item1", + selChangeSeq(null, "lb2_item1"))); + gQueue.push(new synthDownKey("lb2_item1", + selAddSeq("lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthUpKey("lb2_item2", + selRemoveSeq("lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true }, + selRemoveSeq("lb2_item1"))); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302" + title="Incorrect selection events in HTML, XUL and ARIA"> + Bug 414302 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=810268" + title="There's no way to know unselected item when selection in single selection was changed"> + Bug 810268 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="combobox"> + <option id="cb1_item1" value="mushrooms">mushrooms + <option id="cb1_item2" value="greenpeppers">green peppers + <option id="cb1_item3" value="onions" id="onions">onions + <option id="cb1_item4" value="tomatoes">tomatoes + <option id="cb1_item5" value="olives">olives + </select> + + <select id="listbox" size=5> + <option id="lb1_item1" value="mushrooms">mushrooms + <option id="lb1_item2" value="greenpeppers">green peppers + <option id="lb1_item3" value="onions" id="onions">onions + <option id="lb1_item4" value="tomatoes">tomatoes + <option id="lb1_item5" value="olives">olives + </select> + + <p>Pizza</p> + <select id="listbox2" multiple size=5> + <option id="lb2_item1" value="mushrooms">mushrooms + <option id="lb2_item2" value="greenpeppers">green peppers + <option id="lb2_item3" value="onions" id="onions">onions + <option id="lb2_item4" value="tomatoes">tomatoes + <option id="lb2_item5" value="olives">olives + </select> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_selection.xhtml b/accessible/tests/mochitest/events/test_selection.xhtml new file mode 100644 index 0000000000..9c34ddf286 --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection.xhtml @@ -0,0 +1,254 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Selection event tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + function advanceTab(aTabsID, aDirection, aNextTabID) + { + var eventSeq1 = [ + new invokerChecker(EVENT_SELECTION, aNextTabID) + ] + defineScenario(this, eventSeq1); + + var eventSeq2 = [ + new invokerChecker(EVENT_HIDE, getAccessible(aNextTabID)), + new invokerChecker(EVENT_SHOW, aNextTabID) + ]; + defineScenario(this, eventSeq2); + + this.invoke = function advanceTab_invoke() + { + todo(false, "No accessible recreation should happen, just selection event"); + getNode(aTabsID).advanceSelectedTab(aDirection, true); + } + + this.getID = function synthFocus_getID() + { + return "advanceTab on " + prettyName(aTabsID) + " to " + prettyName(aNextTabID); + } + } + + function select4FirstItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(1)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(2)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(3)) + ]; + + this.invoke = function select4FirstItems_invoke() + { + synthesizeKey("VK_DOWN", { shiftKey: true }); // selects two items + synthesizeKey("VK_DOWN", { shiftKey: true }); + synthesizeKey("VK_DOWN", { shiftKey: true }); + } + + this.getID = function select4FirstItems_getID() + { + return "select 4 first items for " + prettyName(aID); + } + } + + function unselect4FirstItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(3)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(2)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(1)), + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0)) + ]; + + this.invoke = function unselect4FirstItems_invoke() + { + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey("VK_UP", { shiftKey: true }); + synthesizeKey(" ", { ctrlKey: true }); // unselect first item + } + + this.getID = function unselect4FirstItems_getID() + { + return "unselect 4 first items for " + prettyName(aID); + } + } + + function selectAllItems(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode)) + ]; + + this.invoke = function selectAllItems_invoke() + { + synthesizeKey("VK_END", { shiftKey: true }); + } + + this.getID = function selectAllItems_getID() + { + return "select all items for " + prettyName(aID); + } + } + + function unselectAllItemsButFirst(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_WITHIN, getAccessible(this.listboxNode)) + ]; + + this.invoke = function unselectAllItemsButFirst_invoke() + { + synthesizeKey("VK_HOME", { shiftKey: true }); + } + + this.getID = function unselectAllItemsButFirst_getID() + { + return "unselect all items for " + prettyName(aID); + } + } + + function unselectSelectItem(aID) + { + this.listboxNode = getNode(aID); + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION_REMOVE, this.listboxNode.getItemAtIndex(0)), + new invokerChecker(EVENT_SELECTION_ADD, this.listboxNode.getItemAtIndex(0)) + ]; + + this.invoke = function unselectSelectItem_invoke() + { + synthesizeKey(" ", { ctrlKey: true }); // select item + synthesizeKey(" ", { ctrlKey: true }); // unselect item + } + + this.getID = function unselectSelectItem_getID() + { + return "unselect and then select first item for " + prettyName(aID); + } + } + + /** + * Do tests. + */ + var gQueue = null; + + //enableLogging("events"); + //gA11yEventDumpToConsole = true; // debuggin + + function doTests() + { + gQueue = new eventQueue(); + + ////////////////////////////////////////////////////////////////////////// + // tabbox + gQueue.push(new advanceTab("tabs", 1, "tab3")); + + ////////////////////////////////////////////////////////////////////////// + // single selection listbox, the first item is selected by default + + gQueue.push(new synthClick("lb1_item2", + new invokerChecker(EVENT_SELECTION, "lb1_item2"))); + gQueue.push(new synthUpKey("lb1_item2", + new invokerChecker(EVENT_SELECTION, "lb1_item1"))); + gQueue.push(new synthDownKey("lb1_item1", + new invokerChecker(EVENT_SELECTION, "lb1_item2"))); + + ////////////////////////////////////////////////////////////////////////// + // multiselectable listbox + gQueue.push(new synthClick("lb2_item1", + new invokerChecker(EVENT_SELECTION, "lb2_item1"))); + gQueue.push(new synthDownKey("lb2_item1", + new invokerChecker(EVENT_SELECTION_ADD, "lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthUpKey("lb2_item2", + new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item2"), + { shiftKey: true })); + gQueue.push(new synthKey("lb2_item1", " ", { ctrlKey: true }, + new invokerChecker(EVENT_SELECTION_REMOVE, "lb2_item1"))); + + ////////////////////////////////////////////////////////////////////////// + // selection event coalescence + + // fire 4 selection_add events + gQueue.push(new select4FirstItems("listbox2")); + // fire 4 selection_remove events + gQueue.push(new unselect4FirstItems("listbox2")); + // fire selection_within event + gQueue.push(new selectAllItems("listbox2")); + // fire selection_within event + gQueue.push(new unselectAllItemsButFirst("listbox2")); + // fire selection_remove/add events + gQueue.push(new unselectSelectItem("listbox2")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=414302" + title="Incorrect selection events in HTML, XUL and ARIA"> + Mozilla Bug 414302 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <tabbox id="tabbox" selectedIndex="1"> + <tabs id="tabs"> + <tab id="tab1" label="tab1"/> + <tab id="tab2" label="tab2"/> + <tab id="tab3" label="tab3"/> + <tab id="tab4" label="tab4"/> + </tabs> + <tabpanels> + <tabpanel><!-- tabpanel First elements go here --></tabpanel> + <tabpanel><button id="b1" label="b1"/></tabpanel> + <tabpanel><button id="b2" label="b2"/></tabpanel> + <tabpanel></tabpanel> + </tabpanels> + </tabbox> + + <richlistbox id="listbox"> + <richlistitem id="lb1_item1"><label value="item1"/></richlistitem> + <richlistitem id="lb1_item2"><label value="item2"/></richlistitem> + </richlistbox> + + <richlistbox id="listbox2" seltype="multiple"> + <richlistitem id="lb2_item1"><label value="item1"/></richlistitem> + <richlistitem id="lb2_item2"><label value="item2"/></richlistitem> + <richlistitem id="lb2_item3"><label value="item3"/></richlistitem> + <richlistitem id="lb2_item4"><label value="item4"/></richlistitem> + <richlistitem id="lb2_item5"><label value="item5"/></richlistitem> + <richlistitem id="lb2_item6"><label value="item6"/></richlistitem> + <richlistitem id="lb2_item7"><label value="item7"/></richlistitem> + </richlistbox> + + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_selection_aria.html b/accessible/tests/mochitest/events/test_selection_aria.html new file mode 100644 index 0000000000..c479868e03 --- /dev/null +++ b/accessible/tests/mochitest/events/test_selection_aria.html @@ -0,0 +1,122 @@ +<html> + +<head> + <title>ARIA selection event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function selectItem(aSelectID, aItemID) { + this.selectNode = getNode(aSelectID); + this.itemNode = getNode(aItemID); + + this.eventSeq = [ + new invokerChecker(EVENT_SELECTION, aItemID), + ]; + + this.invoke = function selectItem_invoke() { + var itemNode = this.selectNode.querySelector("*[aria-selected='true']"); + if (itemNode) + itemNode.removeAttribute("aria-selected"); + + this.itemNode.setAttribute("aria-selected", "true"); + }; + + this.getID = function selectItem_getID() { + return "select item " + prettyName(aItemID); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + var gQueue = null; + + // gA11yEventDumpToConsole = true; // debug stuff + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new selectItem("tablist", "tab1")); + gQueue.push(new selectItem("tablist", "tab2")); + + gQueue.push(new selectItem("tree", "treeitem1")); + gQueue.push(new selectItem("tree", "treeitem1a")); + gQueue.push(new selectItem("tree", "treeitem1a1")); + + gQueue.push(new selectItem("tree2", "tree2item1")); + gQueue.push(new selectItem("tree2", "tree2item1a")); + gQueue.push(new selectItem("tree2", "tree2item1a1")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=569653" + title="Make selection events async"> + Mozilla Bug 569653 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=804040" + title="Selection event not fired when selection of ARIA tab changes"> + Mozilla Bug 804040 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="tablist" id="tablist"> + <div role="tab" id="tab1">tab1</div> + <div role="tab" id="tab2">tab2</div> + </div> + + <div id="tree" role="tree"> + <div id="treeitem1" role="treeitem">Canada + <div id="treeitem1a" role="treeitem">- Ontario + <div id="treeitem1a1" role="treeitem">-- Toronto</div> + </div> + <div id="treeitem1b" role="treeitem">- Manitoba</div> + </div> + <div id="treeitem2" role="treeitem">Germany</div> + <div id="treeitem3" role="treeitem">Russia</div> + </div> + + <div id="tree2" role="tree" aria-multiselectable="true"> + <div id="tree2item1" role="treeitem">Canada + <div id="tree2item1a" role="treeitem">- Ontario + <div id="tree2item1a1" role="treeitem">-- Toronto</div> + </div> + <div id="tree2item1b" role="treeitem">- Manitoba</div> + </div> + <div id="tree2item2" role="treeitem">Germany</div> + <div id="tree2item3" role="treeitem">Russia</div> + </div> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_statechange.html b/accessible/tests/mochitest/events/test_statechange.html new file mode 100644 index 0000000000..9dad6ed2f8 --- /dev/null +++ b/accessible/tests/mochitest/events/test_statechange.html @@ -0,0 +1,317 @@ +<html> + +<head> + <title>Accessible state change event testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function stateChange(aState, aIsExtraState, aIsEnabled, aTarget) { + return [EVENT_STATE_CHANGE, evt => { + evt.QueryInterface(nsIAccessibleStateChangeEvent); + return evt.state == aState && evt.isExtraState == aIsExtraState && + aIsEnabled == evt.isEnabled && getAccessible(aTarget) == evt.accessible; + }]; + } + + async function openNode(aIDDetails, aIDSummary, aIsOpen) { + let p = waitForEvent(...stateChange(STATE_EXPANDED, false, aIsOpen, aIDSummary)); + if (aIsOpen) { + getNode(aIDDetails).setAttribute("open", ""); + } else { + getNode(aIDDetails).removeAttribute("open"); + } + await p; + } + + async function makeEditableDoc(aDocNode, aIsEnabled) { + let p = waitForEvent(...stateChange(EXT_STATE_EDITABLE, true, true, aDocNode)); + aDocNode.designMode = "on"; + await p; + } + + async function invalidInput(aNodeOrID) { + let p = waitForEvent(...stateChange(STATE_INVALID, false, true, aNodeOrID)); + getNode(aNodeOrID).value = "I am not an email"; + await p; + } + + async function changeCheckInput(aID, aIsChecked) { + let p = waitForEvent(...stateChange(STATE_CHECKED, false, aIsChecked, aID)); + getNode(aID).checked = aIsChecked; + await p; + } + + async function changeRequiredState(aID, aIsRequired) { + let p = waitForEvent(...stateChange(STATE_REQUIRED, false, aIsRequired, aID)); + getNode(aID).required = aIsRequired; + await p; + } + + async function stateChangeOnFileInput(aID, aAttr, aValue, + aState, aIsExtraState, aIsEnabled) { + let fileControlNode = getNode(aID); + let fileControl = getAccessible(fileControlNode); + let browseButton = fileControl.firstChild; + let p = waitForEvents([ + stateChange(aState, aIsExtraState, aIsEnabled, fileControl), + stateChange(aState, aIsExtraState, aIsEnabled, browseButton)]) + fileControlNode.setAttribute(aAttr, aValue); + await p; + } + + function toggleSentinel() { + let sentinel = getNode("sentinel"); + if (sentinel.hasAttribute("aria-busy")) { + sentinel.removeAttribute("aria-busy"); + } else { + sentinel.setAttribute("aria-busy", "true"); + } + } + + async function dupeStateChange(aID, aAttr, aValue, + aState, aIsExtraState, aIsEnabled) { + let p = waitForEvents([ + stateChange(aState, aIsExtraState, aIsEnabled, aID), + [EVENT_STATE_CHANGE, "sentinel"] + ]); + getNode(aID).setAttribute(aAttr, aValue); + getNode(aID).setAttribute(aAttr, aValue); + toggleSentinel(); + await p; + } + + async function oppositeStateChange(aID, aAttr, aState, aIsExtraState) { + let p = waitForEvents({ + expected: [[EVENT_STATE_CHANGE, "sentinel"]], + unexpected: [ + stateChange(aState, aIsExtraState, false, aID), + stateChange(aState, aIsExtraState, true, aID) + ] + }); + getNode(aID).setAttribute(aAttr, "false"); + getNode(aID).setAttribute(aAttr, "true"); + toggleSentinel(); + await p; + } + + /** + * Change concomitant ARIA and native attribute at once. + */ + async function echoingStateChange(aID, aARIAAttr, aAttr, aValue, + aState, aIsExtraState, aIsEnabled) { + let p = waitForEvent(...stateChange(aState, aIsExtraState, aIsEnabled, aID)); + if (aValue == null) { + getNode(aID).removeAttribute(aARIAAttr); + getNode(aID).removeAttribute(aAttr); + } else { + getNode(aID).setAttribute(aARIAAttr, aValue); + getNode(aID).setAttribute(aAttr, aValue); + } + await p; + } + + async function testLinked() { + let p = waitForEvent(...stateChange(STATE_LINKED, false, false, "link1")); + getNode("link1").removeAttribute("href"); + await p; + + p = waitForEvent(...stateChange(STATE_LINKED, false, false, "link2")); + getNode("link2").removeAttribute("onclick"); + await p; + + p = waitForEvent(...stateChange(STATE_LINKED, false, true, "link3")); + getNode("link3").setAttribute("href", "http://example.com"); + await p; + } + + async function testHasPopup() { + let p = waitForEvent(...stateChange(STATE_HASPOPUP, false, true, "popupButton")); + getNode("popupButton").setAttribute("aria-haspopup", "true"); + await p; + + p = waitForEvent(...stateChange(STATE_HASPOPUP, false, true, "popupButton")); + getNode("popupButton").setAttribute("aria-haspopup", "tree"); + await p; + + p = waitForEvent(...stateChange(STATE_HASPOPUP, false, true, "popupButton")); + getNode("popupButton").setAttribute("aria-haspopup", "menu"); + await p; + + p = waitForEvent(...stateChange(STATE_HASPOPUP, false, true, "popupButton")); + getNode("popupButton").setAttribute("aria-haspopup", "listbox"); + await p; + + p = waitForEvent(...stateChange(STATE_HASPOPUP, false, true, "popupButton")); + getNode("popupButton").setAttribute("aria-haspopup", "grid"); + await p; + + p = waitForEvent(...stateChange(STATE_HASPOPUP, false, false, "popupButton")); + getNode("popupButton").setAttribute("aria-haspopup", "false"); + await p; + + p = waitForEvent(...stateChange(STATE_HASPOPUP, false, true, "popupButton")); + getNode("popupButton").setAttribute("aria-haspopup", "dialog"); + await p; + + p = waitForEvent(...stateChange(STATE_HASPOPUP, false, false, "popupButton")); + getNode("popupButton").removeAttribute("aria-haspopup"); + await p; + } + + async function doTests() { + // Test opening details objects + await openNode("detailsOpen", "summaryOpen", true); + await openNode("detailsOpen", "summaryOpen", false); + await openNode("detailsOpen1", "summaryOpen1", true); + await openNode("detailsOpen2", "summaryOpen2", true); + await openNode("detailsOpen3", "summaryOpen3", true); + await openNode("detailsOpen4", "summaryOpen4", true); + await openNode("detailsOpen5", "summaryOpen5", true); + await openNode("detailsOpen6", "summaryOpen6", true); + + // Test delayed editable state change + var doc = document.getElementById("iframe").contentDocument; + await makeEditableDoc(doc); + + // invalid state change + await invalidInput("email"); + + // checked state change + await changeCheckInput("checkbox", true); + await changeCheckInput("checkbox", false); + await changeCheckInput("radio", true); + await changeCheckInput("radio", false); + + // required state change + await changeRequiredState("checkbox", true); + + // file input inherited state changes + await stateChangeOnFileInput("file", "aria-busy", "true", + STATE_BUSY, false, true); + await stateChangeOnFileInput("file", "aria-required", "true", + STATE_REQUIRED, false, true); + await stateChangeOnFileInput("file", "aria-invalid", "true", + STATE_INVALID, false, true); + + await dupeStateChange("div", "aria-busy", "true", + STATE_BUSY, false, true); + await oppositeStateChange("div", "aria-busy", + STATE_BUSY, false); + + await echoingStateChange("text1", "aria-disabled", "disabled", "true", + EXT_STATE_ENABLED, true, false); + await echoingStateChange("text1", "aria-disabled", "disabled", null, + EXT_STATE_ENABLED, true, true); + + await testLinked(); + + await testHasPopup(); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> +<style> + details.openBefore::before{ + content: "before detail content: "; + background: blue; + } + summary.openBefore::before{ + content: "before summary content: "; + background: green; + } + details.openAfter::after{ + content: " :after detail content"; + background: blue; + } + summary.openAfter::after{ + content: " :after summary content"; + background: green; + } +</style> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=564471" + title="Make state change events async"> + Bug 564471 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=555728" + title="Fire a11y event based on HTML5 constraint validation"> + Bug 555728 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017" + title="File input control should be propogate states to descendants"> + Bug 699017 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=788389" + title="Fire statechange event whenever checked state is changed not depending on focused state"> + Bug 788389 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=926812" + title="State change event not fired when both disabled and aria-disabled are toggled"> + Bug 926812 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- open --> + <details id="detailsOpen"><summary id="summaryOpen">open</summary>details can be opened</details> + <details id="detailsOpen1">order doesn't matter<summary id="summaryOpen1">open</summary></details> + <details id="detailsOpen2"><div>additional elements don't matter</div><summary id="summaryOpen2">open</summary></details> + <details id="detailsOpen3" class="openBefore"><summary id="summaryOpen3">summary</summary>content</details> + <details id="detailsOpen4" class="openAfter"><summary id="summaryOpen4">summary</summary>content</details> + <details id="detailsOpen5"><summary id="summaryOpen5" class="openBefore">summary</summary>content</details> + <details id="detailsOpen6"><summary id="summaryOpen6" class="openAfter">summary</summary>content</details> + + + <div id="testContainer"> + <iframe id="iframe"></iframe> + </div> + + <input id="email" type='email'> + + <input id="checkbox" type="checkbox"> + <input id="radio" type="radio"> + + <input id="file" type="file"> + + <div id="div"></div> + + <!-- A sentinal guards from events of interest being fired after it emits a state change --> + <div id="sentinel"></div> + + <input id="text1"> + + <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> + + <div id="eventdump"></div> + + <button id="popupButton">action</button> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_statechange_tabpanels.xhtml b/accessible/tests/mochitest/events/test_statechange_tabpanels.xhtml new file mode 100644 index 0000000000..90e8fad75b --- /dev/null +++ b/accessible/tests/mochitest/events/test_statechange_tabpanels.xhtml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="tabpanels state change event tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../promisified-events.js" /> + + <script type="application/javascript"> + <![CDATA[ + function offscreenChangeEvent(acc, enabled) { + return [ + EVENT_STATE_CHANGE, + event => { + const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + return event.accessible == acc && + scEvent.state == STATE_OFFSCREEN && + scEvent.isEnabled == enabled; + } + ]; + } + + async function doTests() { + const tabs = getNode("tabs"); + is(tabs.selectedIndex, 0, "tab1 initially selected"); + const panel1 = getAccessible("panel1"); + testStates(panel1, 0, 0, STATE_OFFSCREEN); + const panel2 = getAccessible("panel2"); + testStates(panel2, STATE_OFFSCREEN); + const panel3 = getAccessible("panel3"); + testStates(panel3, STATE_OFFSCREEN); + + let events = waitForEvents([ + offscreenChangeEvent(panel1, true), + offscreenChangeEvent(panel2, false) + ]); + info("Selecting tab2"); + tabs.selectedIndex = 1; + await events; + + events = waitForEvents([ + offscreenChangeEvent(panel2, true), + offscreenChangeEvent(panel3, false) + ]); + info("Selecting tab3"); + tabs.selectedIndex = 2; + await events; + + events = waitForEvents([ + offscreenChangeEvent(panel3, true), + offscreenChangeEvent(panel1, false) + ]); + info("Selecting tab1"); + tabs.selectedIndex = 0; + await events; + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <tabbox id="tabbox" selectedIndex="0"> + <tabs id="tabs"> + <tab id="tab1" label="tab1"/> + <tab id="tab2" label="tab2"/> + <tab id="tab3" label="tab3"/> + </tabs> + <tabpanels> + <hbox id="panel1"><button label="b1"/></hbox> + <hbox id="panel2"><button label="b2"/></hbox> + <hbox id="panel3"><button label="b3"/></hbox> + </tabpanels> + </tabbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/events/test_text.html b/accessible/tests/mochitest/events/test_text.html new file mode 100644 index 0000000000..8a0bd7f9a4 --- /dev/null +++ b/accessible/tests/mochitest/events/test_text.html @@ -0,0 +1,310 @@ +<html> + +<head> + <title>Accessible mutation events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + /** + * Base text remove invoker and checker. + */ + function textRemoveInvoker(aID, aStart, aEnd, aText) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new textChangeChecker(aID, aStart, aEnd, aText, false), + ]; + } + + function textInsertInvoker(aID, aStart, aEnd, aText) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new textChangeChecker(aID, aStart, aEnd, aText, true), + ]; + } + + /** + * Remove inaccessible child node containing accessibles. + */ + function removeChildSpan(aID) { + this.__proto__ = new textRemoveInvoker(aID, 0, 5, "33322"); + + this.invoke = function removeChildSpan_invoke() { + // remove HTML span, a first child of the node + this.DOMNode.firstChild.remove(); + }; + + this.getID = function removeChildSpan_getID() { + return "Remove inaccessible span containing accessible nodes" + prettyName(aID); + }; + } + + /** + * Insert inaccessible child node containing accessibles. + */ + function insertChildSpan(aID, aInsertAllTogether) { + this.__proto__ = new textInsertInvoker(aID, 0, 5, "33322"); + + this.invoke = function insertChildSpan_invoke() { + // <span><span>333</span><span>22</span></span> + if (aInsertAllTogether) { + let topSpan = document.createElement("span"); + let fSpan = document.createElement("span"); + fSpan.textContent = "333"; + topSpan.appendChild(fSpan); + let sSpan = document.createElement("span"); + sSpan.textContent = "22"; + topSpan.appendChild(sSpan); + + this.DOMNode.insertBefore(topSpan, this.DOMNode.childNodes[0]); + } else { + let topSpan = document.createElement("span"); + this.DOMNode.insertBefore(topSpan, this.DOMNode.childNodes[0]); + + let fSpan = document.createElement("span"); + fSpan.textContent = "333"; + topSpan.appendChild(fSpan); + + let sSpan = document.createElement("span"); + sSpan.textContent = "22"; + topSpan.appendChild(sSpan); + } + }; + + this.getID = function insertChildSpan_getID() { + return "Insert inaccessible span containing accessibles" + + prettyName(aID); + }; + } + + /** + * Remove child embedded accessible. + */ + function removeChildDiv(aID) { + this.__proto__ = new textRemoveInvoker(aID, 5, 6, kEmbedChar); + + this.invoke = function removeChildDiv_invoke() { + var childDiv = this.DOMNode.childNodes[1]; + + // Ensure accessible is created to get text remove event when it's + // removed. + getAccessible(childDiv); + + this.DOMNode.removeChild(childDiv); + }; + + this.getID = function removeChildDiv_getID() { + return "Remove accessible div from the middle of text accessible " + + prettyName(aID); + }; + } + + /** + * Insert child embedded accessible. + */ + function insertChildDiv(aID) { + this.__proto__ = new textInsertInvoker(aID, 5, 6, kEmbedChar); + + this.invoke = function insertChildDiv_invoke() { + var childDiv = document.createElement("div"); + // Note after bug 646216, a sole div without text won't be accessible + // and would not result in an embedded character. + // Therefore, add some text. + childDiv.textContent = "hello"; + this.DOMNode.insertBefore(childDiv, this.DOMNode.childNodes[1]); + }; + + this.getID = function insertChildDiv_getID() { + return "Insert accessible div into the middle of text accessible " + + prettyName(aID); + }; + } + + /** + * Remove children from text container from first to last child or vice + * versa. + */ + function removeChildren(aID, aLastToFirst, aStart, aEnd, aText) { + this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText); + + this.invoke = function removeChildren_invoke() { + if (aLastToFirst) { + while (this.DOMNode.firstChild) + this.DOMNode.removeChild(this.DOMNode.lastChild); + } else { + while (this.DOMNode.firstChild) + this.DOMNode.firstChild.remove(); + } + }; + + this.getID = function removeChildren_getID() { + return "remove children of " + prettyName(aID) + + (aLastToFirst ? " from last to first" : " from first to last"); + }; + } + + /** + * Remove text from HTML input. + */ + function removeTextFromInput(aID, aStart, aEnd, aText) { + this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText); + + this.eventSeq.push(new invokerChecker(EVENT_TEXT_VALUE_CHANGE, + this.DOMNode)); + + this.invoke = function removeTextFromInput_invoke() { + this.DOMNode.focus(); + this.DOMNode.setSelectionRange(aStart, aEnd); + + synthesizeKey("KEY_Delete"); + }; + + this.getID = function removeTextFromInput_getID() { + return "Remove text from " + aStart + " to " + aEnd + " for " + + prettyName(aID); + }; + } + + /** + * Add text into HTML input. + */ + function insertTextIntoInput(aID, aStart, aEnd, aText) { + this.__proto__ = new textInsertInvoker(aID, aStart, aEnd, aText); + + this.eventSeq.push(new invokerChecker(EVENT_TEXT_VALUE_CHANGE, + this.DOMNode)); + + this.invoke = function insertTextIntoInput_invoke() { + this.DOMNode.focus(); + sendString("a"); + }; + + this.getID = function insertTextIntoInput_getID() { + return "Insert text to " + aStart + " for " + prettyName(aID); + }; + } + + /** + * Remove text data from text node of editable area. + */ + function removeTextFromEditable(aID, aStart, aEnd, aText, aTextNode) { + this.__proto__ = new textRemoveInvoker(aID, aStart, aEnd, aText); + + this.invoke = function removeTextFromEditable_invoke() { + this.DOMNode.focus(); + + var selection = window.getSelection(); + var range = document.createRange(); + range.setStart(this.textNode, aStart); + range.setEnd(this.textNode, aEnd); + selection.addRange(range); + + synthesizeKey("KEY_Delete"); + }; + + this.getID = function removeTextFromEditable_getID() { + return "Remove text from " + aStart + " to " + aEnd + " for " + + prettyName(aID); + }; + + this.textNode = getNode(aTextNode); + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + gA11yEventDumpToConsole = true; // debugging + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + // Text remove event on inaccessible child HTML span removal containing + // accessible text nodes. + gQueue.push(new removeChildSpan("p")); + gQueue.push(new insertChildSpan("p"), true); + gQueue.push(new insertChildSpan("p"), false); + + // Remove embedded character. + gQueue.push(new removeChildDiv("div")); + gQueue.push(new insertChildDiv("div")); + + // Remove all children. + var text = kEmbedChar + "txt" + kEmbedChar; + gQueue.push(new removeChildren("div2", true, 0, 5, text)); + gQueue.push(new removeChildren("div3", false, 0, 5, text)); + + // Text remove from text node within hypertext accessible. + gQueue.push(new removeTextFromInput("input", 1, 3, "al")); + gQueue.push(new insertTextIntoInput("input", 1, 2, "a")); + + // bug 570691 + todo(false, "Fix text change events from editable area, see bug 570691"); + // var textNode = getNode("editable").firstChild; + // gQueue.push(new removeTextFromEditable("editable", 1, 3, "al", textNode)); + // textNode = getNode("editable2").firstChild.firstChild; + // gQueue.push(new removeTextFromEditable("editable2", 1, 3, "al", textNode)); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=566293" + title=" wrong length of text remove event when inaccessible node containing accessible nodes is removed"> + Mozilla Bug 566293 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570710" + title="Avoid extra array traversal during text event creation"> + Mozilla Bug 570710 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=574003" + title="Coalesce text events on nodes removal"> + Mozilla Bug 574003 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=575052" + title="Cache text offsets within hypertext accessible"> + Mozilla Bug 575052 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275" + title="Rework accessible tree update code"> + Mozilla Bug 570275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="p"><span><span>333</span><span>22</span></span>1111</p> + <div id="div">hello<div>hello</div>hello</div> + <div id="div2"><div>txt</div>txt<div>txt</div></div> + <div id="div3"><div>txt</div>txt<div>txt</div></div> + <input id="input" value="value"> + <div contentEditable="true" id="editable">value</div> + <div contentEditable="true" id="editable2"><span>value</span></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_text_alg.html b/accessible/tests/mochitest/events/test_text_alg.html new file mode 100644 index 0000000000..b270c6a7d0 --- /dev/null +++ b/accessible/tests/mochitest/events/test_text_alg.html @@ -0,0 +1,246 @@ +<html> + +<head> + <title>Accessible text update algorithm testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + const kRemoval = false; + const kInsertion = true; + const kUnexpected = true; + + function changeText(aContainerID, aValue, aEventList) { + this.containerNode = getNode(aContainerID); + this.textNode = this.containerNode.firstChild; + this.textData = this.textNode.data; + + this.eventSeq = [ ]; + this.unexpectedEventSeq = [ ]; + + for (var i = 0; i < aEventList.length; i++) { + var event = aEventList[i]; + + var isInserted = event[0]; + var str = event[1]; + var offset = event[2]; + var checker = new textChangeChecker(this.containerNode, offset, + offset + str.length, str, + isInserted); + + if (event[3] == kUnexpected) + this.unexpectedEventSeq.push(checker); + else + this.eventSeq.push(checker); + } + + this.invoke = function changeText_invoke() { + this.textNode.data = aValue; + }; + + this.getID = function changeText_getID() { + return "change text '" + shortenString(this.textData) + "' -> '" + + shortenString(this.textNode.data) + "' for " + + prettyName(this.containerNode); + }; + } + + function expStr(x, doublings) { + for (var i = 0; i < doublings; ++i) + x = x + x; + return x; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + + // //////////////////////////////////////////////////////////////////////// + // wqrema -> tqb: substitution coalesced with removal + + var events = [ + [ kRemoval, "w", 0 ], // wqrema -> qrema + [ kInsertion, "t", 0], // qrema -> tqrema + [ kRemoval, "rema", 2 ], // tqrema -> tq + [ kInsertion, "b", 2], // tq -> tqb + ]; + gQueue.push(new changeText("p1", "tqb", events)); + + // //////////////////////////////////////////////////////////////////////// + // b -> insa: substitution coalesced with insertion (complex substitution) + + events = [ + [ kRemoval, "b", 0 ], // b -> + [ kInsertion, "insa", 0], // -> insa + ]; + gQueue.push(new changeText("p2", "insa", events)); + + // //////////////////////////////////////////////////////////////////////// + // abc -> def: coalesced substitutions + + events = [ + [ kRemoval, "abc", 0 ], // abc -> + [ kInsertion, "def", 0], // -> def + ]; + gQueue.push(new changeText("p3", "def", events)); + + // //////////////////////////////////////////////////////////////////////// + // abcabc -> abcDEFabc: coalesced insertions + + events = [ + [ kInsertion, "DEF", 3], // abcabc -> abcDEFabc + ]; + gQueue.push(new changeText("p4", "abcDEFabc", events)); + + // //////////////////////////////////////////////////////////////////////// + // abc -> defabc: insertion into begin + + events = [ + [ kInsertion, "def", 0], // abc -> defabc + ]; + gQueue.push(new changeText("p5", "defabc", events)); + + // //////////////////////////////////////////////////////////////////////// + // abc -> abcdef: insertion into end + + events = [ + [ kInsertion, "def", 3], // abc -> abcdef + ]; + gQueue.push(new changeText("p6", "abcdef", events)); + + // //////////////////////////////////////////////////////////////////////// + // defabc -> abc: removal from begin + + events = [ + [ kRemoval, "def", 0], // defabc -> abc + ]; + gQueue.push(new changeText("p7", "abc", events)); + + // //////////////////////////////////////////////////////////////////////// + // abcdef -> abc: removal from the end + + events = [ + [ kRemoval, "def", 3], // abcdef -> abc + ]; + gQueue.push(new changeText("p8", "abc", events)); + + // //////////////////////////////////////////////////////////////////////// + // abcDEFabc -> abcabc: coalesced removals + + events = [ + [ kRemoval, "DEF", 3], // abcDEFabc -> abcabc + ]; + gQueue.push(new changeText("p9", "abcabc", events)); + + // //////////////////////////////////////////////////////////////////////// + // !abcdef@ -> @axbcef!: insertion, deletion and substitutions + + events = [ + [ kRemoval, "!", 0 ], // !abcdef@ -> abcdef@ + [ kInsertion, "@", 0], // abcdef@ -> @abcdef@ + [ kInsertion, "x", 2 ], // @abcdef@ -> @axbcdef@ + [ kRemoval, "d", 5], // @axbcdef@ -> @axbcef@ + [ kRemoval, "@", 7 ], // @axbcef@ -> @axbcef + [ kInsertion, "!", 7 ], // @axbcef -> @axbcef! + ]; + gQueue.push(new changeText("p10", "@axbcef!", events)); + + // //////////////////////////////////////////////////////////////////////// + // meilenstein -> levenshtein: insertion, complex and simple substitutions + + events = [ + [ kRemoval, "m", 0 ], // meilenstein -> eilenstein + [ kInsertion, "l", 0], // eilenstein -> leilenstein + [ kRemoval, "il", 2 ], // leilenstein -> leenstein + [ kInsertion, "v", 2], // leenstein -> levenstein + [ kInsertion, "h", 6 ], // levenstein -> levenshtein + ]; + gQueue.push(new changeText("p11", "levenshtein", events)); + + // //////////////////////////////////////////////////////////////////////// + // long strings, remove/insert pair as the old string was replaced on + // new one + + var longStr1 = expStr("x", 16); + var longStr2 = expStr("X", 16); + + var newStr = "a" + longStr1 + "b", insStr = longStr1, rmStr = ""; + events = [ + [ kRemoval, rmStr, 1, kUnexpected ], + [ kInsertion, insStr, 1 ], + ]; + gQueue.push(new changeText("p12", newStr, events)); + + newStr = "a" + longStr2 + "b"; + insStr = longStr2; + rmStr = longStr1; + events = [ + [ kRemoval, rmStr, 1 ], + [ kInsertion, insStr, 1], + ]; + gQueue.push(new changeText("p12", newStr, events)); + + newStr = "ab"; + insStr = ""; + rmStr = longStr2; + events = [ + [ kRemoval, rmStr, 1 ], + [ kInsertion, insStr, 1, kUnexpected ], + ]; + gQueue.push(new changeText("p12", newStr, events)); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660" + title="Cache rendered text on a11y side"> + Mozilla Bug 626660 + </a> + <br> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <p id="p1">wqrema</p> + <p id="p2">b</p> + <p id="p3">abc</p> + <p id="p4">abcabc</p> + <p id="p5">abc</p> + <p id="p6">abc</p> + <p id="p7">defabc</p> + <p id="p8">abcdef</p> + <p id="p9">abcDEFabc</p> + <p id="p10">!abcdef@</p> + <p id="p11">meilenstein</p> + <p id="p12">ab</p> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_textattrchange.html b/accessible/tests/mochitest/events/test_textattrchange.html new file mode 100644 index 0000000000..6282ceb5c2 --- /dev/null +++ b/accessible/tests/mochitest/events/test_textattrchange.html @@ -0,0 +1,105 @@ +<html> + +<head> + <title>Text attribute changed event for misspelled text</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + + const {InlineSpellChecker} = ChromeUtils.import("resource://gre/modules/InlineSpellChecker.jsm"); + + function spelledTextInvoker(aID) { + this.DOMNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_TEXT_ATTRIBUTE_CHANGED, this.DOMNode), + ]; + + this.invoke = function spelledTextInvoker_invoke() { + var editor = this.DOMNode.editor; + var spellChecker = new InlineSpellChecker(editor); + spellChecker.enabled = true; + + // var spellchecker = editor.getInlineSpellChecker(true); + // spellchecker.enableRealTimeSpell = true; + + this.DOMNode.value = "valid text inalid tixt"; + }; + + this.finalCheck = function spelledTextInvoker_finalCheck() { + var defAttrs = buildDefaultTextAttrs(this.DOMNode, kInputFontSize, + kNormalFontWeight, + kInputFontFamily); + testDefaultTextAttrs(aID, defAttrs); + + var attrs = { }; + var misspelledAttrs = { + "invalid": "spelling", + }; + + testTextAttrs(aID, 0, attrs, defAttrs, 0, 11); + testTextAttrs(aID, 11, misspelledAttrs, defAttrs, 11, 17); + testTextAttrs(aID, 17, attrs, defAttrs, 17, 18); + testTextAttrs(aID, 18, misspelledAttrs, defAttrs, 18, 22); + }; + + this.getID = function spelledTextInvoker_getID() { + return "text attribute change for misspelled text"; + }; + } + + /** + * Do tests. + */ + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTests() { + // Synth focus before spellchecking turning on to make sure editor + // gets a time for initialization. + + gQueue = new eventQueue(); + gQueue.push(new synthFocus("input")); + gQueue.push(new spelledTextInvoker("input")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=345759" + title="Implement text attributes"> + Mozilla Bug 345759 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input"/> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_textselchange.html b/accessible/tests/mochitest/events/test_textselchange.html new file mode 100644 index 0000000000..3dce0760eb --- /dev/null +++ b/accessible/tests/mochitest/events/test_textselchange.html @@ -0,0 +1,82 @@ +<html> + +<head> + <title>Accessible text selection change events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + var gQueue = null; + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function getOnclickSeq(aID) { + return [ + new caretMoveChecker(0, true, aID), + new unexpectedInvokerChecker(EVENT_TEXT_SELECTION_CHANGED, aID), + ]; + } + + function doTests() { + // test caret move events and caret offsets + gQueue = new eventQueue(); + + gQueue.push(new synthClick("c1_p1", getOnclickSeq("c1_p1"))); + gQueue.push(new synthDownKey("c1", new textSelectionChecker("c1", 0, 1, "c1_p1", 0, "c1_p2", 0), { shiftKey: true })); + gQueue.push(new synthDownKey("c1", new textSelectionChecker("c1", 0, 2, "c1_p1", 0, "c1_p2", 9), { shiftKey: true })); + + gQueue.push(new synthClick("ta1", getOnclickSeq("ta1"))); + gQueue.push(new synthRightKey("ta1", + new textSelectionChecker("ta1", 0, 1, "ta1", 0, "ta1", 1), + { shiftKey: true })); + gQueue.push(new synthLeftKey("ta1", + [new textSelectionChecker("ta1", 0, 0, "ta1", 0, "ta1", 0), + new caretMoveChecker(0, true, "ta1")])); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=762934" + title="Text selection change event has a wrong target when selection is spanned through several objects"> + Bug 762934 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=956032" + title="Text selection change event missed when selected text becomes unselected"> + Bug 956032 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="c1" contentEditable="true"> + <p id="c1_p1">paragraph</p> + <p id="c1_p2">paragraph</p> + </div> + + <textarea id="ta1">Hello world</textarea> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/events/test_tree.xhtml b/accessible/tests/mochitest/events/test_tree.xhtml new file mode 100644 index 0000000000..af7feafde8 --- /dev/null +++ b/accessible/tests/mochitest/events/test_tree.xhtml @@ -0,0 +1,358 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="DOM TreeRowCountChanged and a11y name change events."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + var gView; + + //////////////////////////////////////////////////////////////////////////// + // Invoker's checkers + + /** + * Check TreeRowCountChanged event. + */ + function rowCountChangedChecker(aMsg, aIdx, aCount) + { + this.type = "TreeRowCountChanged"; + this.target = gTree; + this.check = function check(aEvent) + { + var propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2); + var index = propBag.getPropertyAsInt32("index"); + is(index, aIdx, "Wrong 'index' data of 'treeRowCountChanged' event."); + + var count = propBag.getPropertyAsInt32("count"); + is(count, aCount, "Wrong 'count' data of 'treeRowCountChanged' event."); + } + this.getID = function getID() + { + return aMsg + "TreeRowCountChanged"; + } + } + + /** + * Check TreeInvalidated event. + */ + function treeInvalidatedChecker(aMsg, aStartRow, aEndRow, aStartCol, aEndCol) + { + this.type = "TreeInvalidated"; + this.target = gTree; + this.check = function check(aEvent) + { + var propBag = aEvent.detail.QueryInterface(Ci.nsIPropertyBag2); + try { + var startRow = propBag.getPropertyAsInt32("startrow"); + } catch (e) { + if (e.name != 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + startRow = null; + } + is(startRow, aStartRow, + "Wrong 'startrow' of 'treeInvalidated' event on " + aMsg); + + try { + var endRow = propBag.getPropertyAsInt32("endrow"); + } catch (e) { + if (e.name != 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + endRow = null; + } + is(endRow, aEndRow, + "Wrong 'endrow' of 'treeInvalidated' event on " + aMsg); + + try { + var startCol = propBag.getPropertyAsInt32("startcolumn"); + } catch (e) { + if (e.name != 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + startCol = null; + } + is(startCol, aStartCol, + "Wrong 'startcolumn' of 'treeInvalidated' event on " + aMsg); + + try { + var endCol = propBag.getPropertyAsInt32("endcolumn"); + } catch (e) { + if (e.name != 'NS_ERROR_NOT_AVAILABLE') { + throw e; + } + endCol = null; + } + is(endCol, aEndCol, + "Wrong 'endcolumn' of 'treeInvalidated' event on " + aMsg); + } + this.getID = function getID() + { + return "TreeInvalidated on " + aMsg; + } + } + + /** + * Check name changed a11y event. + */ + function nameChangeChecker(aMsg, aRow, aCol) + { + this.type = EVENT_NAME_CHANGE; + + function targetGetter() + { + var acc = getAccessible(gTree); + + var tableAcc = getAccessible(acc, [nsIAccessibleTable]); + return tableAcc.getCellAt(aRow, aCol); + } + Object.defineProperty(this, "target", { get: targetGetter }); + + this.getID = function getID() + { + return aMsg + "name changed"; + } + } + + /** + * Check name changed a11y event for a row. + */ + function rowNameChangeChecker(aMsg, aRow) + { + this.type = EVENT_NAME_CHANGE; + + function targetGetter() + { + var acc = getAccessible(gTree); + return acc.getChildAt(aRow + 1); + } + Object.defineProperty(this, "target", { get: targetGetter }); + + this.getID = function getID() + { + return aMsg + "name changed"; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Invokers + + /** + * Set tree view. + */ + function setTreeView() + { + this.invoke = function setTreeView_invoke() + { + gTree.view = gView; + } + + this.getID = function setTreeView_getID() { return "set tree view"; } + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, gTree) + ]; + }; + + /** + * Insert row at 0 index and checks TreeRowCountChanged and TreeInvalidated + * event. + */ + function insertRow() + { + this.invoke = function insertRow_invoke() + { + gView.appendItem("last"); + gTree.rowCountChanged(0, 1); + } + + this.eventSeq = + [ + new rowCountChangedChecker("insertRow: ", 0, 1), + new treeInvalidatedChecker("insertRow", 0, 5, null, null) + ]; + + this.getID = function insertRow_getID() + { + return "insert row"; + } + } + + /** + * Invalidates first column and checks six name changed events for each + * treeitem plus TreeInvalidated event. + */ + function invalidateColumn() + { + this.invoke = function invalidateColumn_invoke() + { + // Make sure accessible subtree of XUL tree is created otherwise no + // name change events for cell accessibles are emitted. + var tree = getAccessible(gTree); + var child = tree.firstChild; + var walkDown = true; + while (child != tree) { + if (walkDown) { + var grandChild = child.firstChild; + if (grandChild) { + child = grandChild; + continue; + } + } + + var sibling = child.nextSibling; + if (sibling) { + child = sibling; + walkDown = true; + continue; + } + + child = child.parent; + walkDown = false; + } + + // Fire 'TreeInvalidated' event by InvalidateColumn() + var firstCol = gTree.columns.getFirstColumn(); + for (var i = 0; i < gView.rowCount; i++) + gView.setCellText(i, firstCol, "hey " + String(i) + "x0"); + + gTree.invalidateColumn(firstCol); + } + + this.eventSeq = + [ + new nameChangeChecker("invalidateColumn: ", 0, 0), + new nameChangeChecker("invalidateColumn: ", 1, 0), + new nameChangeChecker("invalidateColumn: ", 2, 0), + new nameChangeChecker("invalidateColumn: ", 3, 0), + new nameChangeChecker("invalidateColumn: ", 4, 0), + new nameChangeChecker("invalidateColumn: ", 5, 0), + new treeInvalidatedChecker("invalidateColumn", null, null, 0, 0) + ]; + + this.getID = function invalidateColumn_getID() + { + return "invalidate column"; + } + } + + /** + * Invalidates second row and checks name changed event for first treeitem + * (note, there are two name changed events on linux due to different + * accessible tree for xul:tree element) plus TreeInvalidated event. + */ + function invalidateRow() + { + this.invoke = function invalidateRow_invoke() + { + // Fire 'TreeInvalidated' event by InvalidateRow() + // eslint-disable-next-line no-unused-vars + var colCount = gTree.columns.count; + var column = gTree.columns.getFirstColumn(); + while (column) { + gView.setCellText(1, column, "aloha 1x" + String(column.index)); + column = column.getNext(); + } + + gTree.invalidateRow(1); + } + + this.eventSeq = + [ + new nameChangeChecker("invalidateRow: ", 1, 0), + new nameChangeChecker("invalidateRow: ", 1, 1), + new rowNameChangeChecker("invalidateRow: ", 1), + new treeInvalidatedChecker("invalidateRow", 1, 1, null, null) + ]; + + this.getID = function invalidateRow_getID() + { + return "invalidate row"; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + var gTree = null; + var gTreeView = null; + var gQueue = null; + + // gA11yEventDumpID = "debug"; + gA11yEventDumpToConsole = true; // debuggin + + function doTest() + { + // Initialize the tree + gTree = document.getElementById("tree"); + gView = new nsTableTreeView(5); + + // Perform actions + gQueue = new eventQueue(); + + gQueue.push(new setTreeView()); + gQueue.push(new insertRow()); + gQueue.push(new invalidateColumn()); + gQueue.push(new invalidateRow()); + + gQueue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=368835" + title="Fire TreeViewChanged/TreeRowCountChanged events."> + Mozilla Bug 368835 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=308564" + title="No accessibility events when data in a tree row changes."> + Mozilla Bug 308564 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=739524" + title="replace TreeViewChanged DOM event on direct call from XUL tree."> + Mozilla Bug 739524 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=743568" + title="Thunderbird message list tree emitting incorrect focus signals after message deleted."> + Mozilla Bug 743568 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="debug"/> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col1" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/events/test_valuechange.html b/accessible/tests/mochitest/events/test_valuechange.html new file mode 100644 index 0000000000..bf87580a2d --- /dev/null +++ b/accessible/tests/mochitest/events/test_valuechange.html @@ -0,0 +1,279 @@ +<html> + +<head> + <title>Accessible value change events testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript" + src="../value.js"></script> + + <script type="application/javascript"> + /** + * Do tests. + */ + var gQueue = null; + + // Value change invoker + function changeARIAValue(aNodeOrID, aValuenow, aValuetext) { + this.DOMNode = getNode(aNodeOrID); + this.eventSeq = [ new invokerChecker(aValuetext ? + EVENT_TEXT_VALUE_CHANGE : + EVENT_VALUE_CHANGE, this.DOMNode), + ]; + + this.invoke = function changeARIAValue_invoke() { + // Note: this should not fire an EVENT_VALUE_CHANGE when aria-valuetext + // is not empty + if (aValuenow != undefined) + this.DOMNode.setAttribute("aria-valuenow", aValuenow); + + // Note: this should always fire an EVENT_VALUE_CHANGE + if (aValuetext != undefined) + this.DOMNode.setAttribute("aria-valuetext", aValuetext); + }; + + this.check = function changeARIAValue_check() { + var acc = getAccessible(aNodeOrID, [nsIAccessibleValue]); + if (!acc) + return; + + // Note: always test against valuetext first because the existence of + // aria-valuetext takes precedence over aria-valuenow in gecko. + is(acc.value, (aValuetext != undefined) ? aValuetext : aValuenow, + "Wrong value of " + prettyName(aNodeOrID)); + }; + + this.getID = function changeARIAValue_getID() { + return prettyName(aNodeOrID) + " value changed"; + }; + } + + function changeValue(aID, aValue) { + this.DOMNode = getNode(aID); + this.eventSeq = [new invokerChecker(EVENT_TEXT_VALUE_CHANGE, + this.DOMNode), + ]; + + this.invoke = function changeValue_invoke() { + this.DOMNode.value = aValue; + }; + + this.check = function changeValue_check() { + var acc = getAccessible(this.DOMNode); + is(acc.value, aValue, "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeValue_getID() { + return prettyName(aID) + " value changed"; + }; + } + + function changeProgressValue(aID, aValue) { + this.DOMNode = getNode(aID); + this.eventSeq = [new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode)]; + + this.invoke = function changeProgressValue_invoke() { + this.DOMNode.value = aValue; + }; + + this.check = function changeProgressValue_check() { + var acc = getAccessible(this.DOMNode); + is(acc.value, aValue + "%", "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeProgressValue_getID() { + return prettyName(aID) + " value changed"; + }; + } + + function changeRangeValue(aID) { + this.DOMNode = getNode(aID); + this.eventSeq = [new invokerChecker(EVENT_VALUE_CHANGE, this.DOMNode)]; + + this.invoke = function changeRangeValue_invoke() { + synthesizeMouse(getNode(aID), 5, 5, { }); + }; + + this.finalCheck = function changeRangeValue_finalCheck() { + var acc = getAccessible(this.DOMNode); + is(acc.value, "0", "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeRangeValue_getID() { + return prettyName(aID) + " range value changed"; + }; + } + + function changeSelectValue(aID, aKey, aValue) { + this.eventSeq = + [ new invokerChecker(EVENT_TEXT_VALUE_CHANGE, getAccessible(aID)) ]; + + this.invoke = function changeSelectValue_invoke() { + getAccessible(aID).takeFocus(); + synthesizeKey(aKey, {}, window); + }; + + this.finalCheck = function changeSelectValue_finalCheck() { + is(getAccessible(aID).value, aValue, "Wrong value for " + prettyName(aID)); + }; + + this.getID = function changeSelectValue_getID() { + return `${prettyName(aID)} closed select value change on '${aKey}'' key press`; + }; + } + + // enableLogging("DOMEvents"); + // gA11yEventDumpToConsole = true; + function doTests() { + // Test initial values + testValue("slider_vn", "5", 5, 0, 1000, 0); + testValue("slider_vnvt", "plain", 0, 0, 5, 0); + testValue("slider_vt", "hi", 1.5, 0, 3, 0); + testValue("scrollbar", "5", 5, 0, 1000, 0); + testValue("splitter", "5", 5, 0, 1000, 0); + testValue("progress", "22%", 22, 0, 100, 0); + testValue("range", "6", 6, 0, 10, 1); + + // Test that elements which should not expose values do not + let separatorVal = getAccessible("separator", [nsIAccessibleValue], null, DONOTFAIL_IF_NO_INTERFACE); + ok(!separatorVal, "value interface is not exposed for separator"); + let separatorAcc = getAccessible("separator"); + ok(!separatorAcc.value, "Value text is not exposed for separator"); + + // Test value change events + gQueue = new eventQueue(); + + gQueue.push(new changeARIAValue("slider_vn", "6", undefined)); + gQueue.push(new changeARIAValue("slider_vt", undefined, "hey!")); + gQueue.push(new changeARIAValue("slider_vnvt", "3", "sweet")); + gQueue.push(new changeARIAValue("scrollbar", "6", undefined)); + gQueue.push(new changeARIAValue("splitter", "6", undefined)); + + gQueue.push(new changeValue("combobox", "hello")); + + gQueue.push(new changeProgressValue("progress", "50")); + gQueue.push(new changeRangeValue("range")); + + gQueue.push(new changeSelectValue("select", "VK_DOWN", "2nd")); + gQueue.push(new changeSelectValue("select", "3", "3rd")); + + let iframeSelect = getAccessible("selectIframe").firstChild.firstChild; + gQueue.push(new changeSelectValue(iframeSelect, "VK_DOWN", "2")); + + let shadowSelect = getAccessible("selectShadow").firstChild; + gQueue.push(new changeSelectValue(shadowSelect, "VK_DOWN", "2")); + + gQueue.push(new changeValue("number", "2")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=478032" + title=" Fire delayed value changed event for aria-valuetext changes"> + Mozilla Bug 478032 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=529289" + title="We dont expose new aria role 'scrollbar' and property aria-orientation"> + Mozilla Bug 529289 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559764" + title="Make HTML5 input@type=range element accessible"> + Mozilla Bug 559764 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=703202" + title="ARIA comboboxes don't fire value change events"> + Mozilla Bug 703202 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=761901" + title=" HTML5 progress accessible should fire value change event"> + Mozilla Bug 761901 + </a> + + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <!-- ARIA sliders --> + <div id="slider_vn" role="slider" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="1000">slider</div> + + <div id="slider_vt" role="slider" aria-valuetext="hi" + aria-valuemin="0" aria-valuemax="3">greeting slider</div> + + <div id="slider_vnvt" role="slider" aria-valuenow="0" aria-valuetext="plain" + aria-valuemin="0" aria-valuemax="5">sweetness slider</div> + + <!-- ARIA scrollbar --> + <div id="scrollbar" role="scrollbar" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="1000">slider</div> + + <!-- ARIA separator which is focusable (i.e. a splitter) --> + <div id="splitter" role="separator" tabindex="0" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="1000">splitter</div> + + <!-- ARIA separator which is not focusable and should not expose values --> + <div id="separator" role="separator" aria-valuenow="5" + aria-valuemin="0" aria-valuemax="1000">splitter</div> + + <!-- ARIA combobox --> + <input id="combobox" role="combobox" aria-autocomplete="inline"> + + <!-- progress bar --> + <progress id="progress" value="22" max="100"></progress> + + <!-- input@type="range" --> + <input type="range" id="range" min="0" max="10" value="6"> + + <select id="select"> + <option>1st</option> + <option>2nd</option> + <option>3rd</option> + </select> + + <iframe id="selectIframe" + src="data:text/html,<select id='iframeSelect'><option>1</option><option>2</option></select>"> + </iframe> + + <div id="selectShadow"></div> + <script> + let host = document.getElementById("selectShadow"); + let shadow = host.attachShadow({mode: "open"}); + let select = document.createElement("select"); + select.id = "shadowSelect"; + let option = document.createElement("option"); + option.textContent = "1"; + select.appendChild(option); + option = document.createElement("option"); + option.textContent = "2"; + select.appendChild(option); + shadow.appendChild(select); + </script> + + <input type="number" id="number" value="1"> +</body> +</html> diff --git a/accessible/tests/mochitest/focus/a11y.ini b/accessible/tests/mochitest/focus/a11y.ini new file mode 100644 index 0000000000..905d38882c --- /dev/null +++ b/accessible/tests/mochitest/focus/a11y.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_focus_radio.xhtml] +[test_focusedChild.html] +skip-if = (os == 'win' && os_version != '6.1') # bug 845134 +[test_takeFocus.html] +[test_takeFocus.xhtml] diff --git a/accessible/tests/mochitest/focus/test_focus_radio.xhtml b/accessible/tests/mochitest/focus/test_focus_radio.xhtml new file mode 100644 index 0000000000..717d9976b6 --- /dev/null +++ b/accessible/tests/mochitest/focus/test_focus_radio.xhtml @@ -0,0 +1,84 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Tests for Accessible TakeFocus on Radio Elements"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../promisified-events.js" /> + + <script type="application/javascript"> + <![CDATA[ + async function doTests() { + let radio1 = getAccessible("radio1"); + let focused = waitForEvent(EVENT_FOCUS, radio1); + radio1.takeFocus(); + await focused; + // radio1 wasn't selected. Ensure that takeFocus didn't change that. + testStates(radio1, STATE_FOCUSED, 0, STATE_CHECKED); + + // Test focusing another radio in the group while the group is still + // focused. + let radio2 = getAccessible("radio2"); + focused = waitForEvent(EVENT_FOCUS, radio2); + radio2.takeFocus(); + await focused; + testStates(radio2, STATE_FOCUSED | STATE_CHECKED); + + let groupEl = document.getElementById("radiogroup"); + // Selecting an item also focuses it. + focused = waitForEvent(EVENT_FOCUS, radio1); + groupEl.value = "1"; + await focused; + testStates(radio1, STATE_FOCUSED | STATE_CHECKED); + + // If an item is already selected but not focused, selecting it again + // focuses it. + focused = waitForEvent(EVENT_FOCUS, radio2); + radio2.takeFocus(); + await focused; + testStates(radio2, STATE_FOCUSED, 0, STATE_CHECKED); + // radio1 is selected but not focused. + // Select radio1 again, which should focus it. + focused = waitForEvent(EVENT_FOCUS, radio1); + groupEl.value = "1"; + await focused; + testStates(radio1, STATE_FOCUSED | STATE_CHECKED); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <radiogroup id="radiogroup" value="2"> + <radio id="radio1" value="1"/> + <radio id="radio2" value="2"/> + </radiogroup> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/focus/test_focusedChild.html b/accessible/tests/mochitest/focus/test_focusedChild.html new file mode 100644 index 0000000000..d12e229b48 --- /dev/null +++ b/accessible/tests/mochitest/focus/test_focusedChild.html @@ -0,0 +1,81 @@ +<html> + +<head> + <title>nsIAccessible::focusedChild testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function openWnd() { + this.eventSeq = [ new invokerChecker(EVENT_FOCUS, + getDialogAccessible, + this) ]; + + this.invoke = function openWnd_invoke() { + this.dialog = window.browsingContext.topChromeWindow + .openDialog("about:mozilla", + "AboutMozilla", + "chrome,width=600,height=600"); + }; + + this.finalCheck = function openWnd_finalCheck() { + var app = getApplicationAccessible(); + is(app.focusedChild, getDialogAccessible(this), + "Wrong focused child"); + + this.dialog.close(); + }; + + this.getID = function openWnd_getID() { + return "focusedChild for application accessible"; + }; + + function getDialogAccessible(aInvoker) { + return getAccessible(aInvoker.dialog.document); + } + } + + gA11yEventDumpToConsole = true; + var gQueue = null; + + function doTest() { + enableLogging("focus,doclifecycle"); + gQueue = new eventQueue(); + + gQueue.push(new openWnd()); + + gQueue.onFinish = function() { disableLogging(); }; + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=677467" + title="focusedChild crashes on application accessible"> + Mozilla Bug 677467 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + +</body> +</html> diff --git a/accessible/tests/mochitest/focus/test_takeFocus.html b/accessible/tests/mochitest/focus/test_takeFocus.html new file mode 100644 index 0000000000..752ed66b36 --- /dev/null +++ b/accessible/tests/mochitest/focus/test_takeFocus.html @@ -0,0 +1,109 @@ +<html> + +<head> + <title>nsIAccessible::takeFocus testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function takeFocusInvoker(aID) { + this.accessible = getAccessible(aID); + + this.eventSeq = [ new focusChecker(this.accessible) ]; + + this.invoke = function takeFocusInvoker_invoke() { + this.accessible.takeFocus(); + }; + + this.getID = function takeFocusInvoker_getID() { + return "takeFocus for " + prettyName(aID); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTest() { + disableLogging(); // from test_focusedChild + gQueue = new eventQueue(); + + gQueue.push(new takeFocusInvoker("aria-link")); + gQueue.push(new takeFocusInvoker("aria-link2")); + gQueue.push(new takeFocusInvoker("link")); + gQueue.push(new takeFocusInvoker("item2")); + gQueue.push(new takeFocusInvoker(document)); + gQueue.push(new takeFocusInvoker("lb_item2")); + gQueue.push(new takeFocusInvoker(document)); + gQueue.push(new takeFocusInvoker("lb_item3.2")); + gQueue.push(new takeFocusInvoker(document)); + gQueue.push(new takeFocusInvoker("lb_item3.1")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=429547" + title="Support aria-activedescendant usage in nsIAccesible::TakeFocus()"> + Mozilla Bug 429547 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=452710" + title="nsIAccessible::takeFocus testing"> + Mozilla Bug 452710 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=706067" + title="Make takeFocus work on widget items"> + Mozilla Bug 706067 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <span id="aria-link" role="link" tabindex="0">link</span> + <span id="aria-link2" role="link" tabindex="0">link</span> + + <a id="link" href="">link</a> + + <div role="listbox" aria-activedescendant="item1" id="container" tabindex="1"> + <div role="option" id="item1">item1</div> + <div role="option" id="item2">item2</div> + <div role="option" id="item3">item3</div> + </div> + + <select id="listbox" size="5"> + <option id="lb_item1">item1</option> + <option id="lb_item2">item2</option> + <optgroup> + <option id="lb_item3.1">item 3.1</option> + <option id="lb_item3.2">item 3.2</option> + </optgroup> + </select> +</body> +</html> diff --git a/accessible/tests/mochitest/focus/test_takeFocus.xhtml b/accessible/tests/mochitest/focus/test_takeFocus.xhtml new file mode 100644 index 0000000000..127f47d067 --- /dev/null +++ b/accessible/tests/mochitest/focus/test_takeFocus.xhtml @@ -0,0 +1,104 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible focus testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function takeFocusInvoker(aID, aArgConverterFunc) + { + this.targetFunc = aArgConverterFunc ? aArgConverterFunc : getAccessible; + + this.eventSeq = [ new focusChecker(this.targetFunc, aID) ]; + + this.invoke = function takeFocusInvoker_invoke() + { + this.targetFunc.call(null, aID).takeFocus(); + } + + this.getID = function takeFocusInvoker_getID() + { + return "takeFocus for " + prettyName(aID); + } + } + + function getLastChild(aID) + { + return getAccessible(aID).lastChild; + } + + //////////////////////////////////////////////////////////////////////////// + // Tests + + //gA11yEventDumpID = "eventdump"; // debug stuff + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTests() + { + // Test focus events. + gQueue = new eventQueue(); + + gQueue.push(new takeFocusInvoker("tree", getLastChild)); + gQueue.push(new takeFocusInvoker("listitem2")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTests, "tree", new nsTableTreeView(5)); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=706067" + title="Make takeFocus work on widget items"> + Mozilla Bug 706067 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col1" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + + <richlistbox id="listbox"> + <richlistitem id="listitem1"><label value="item1"/></richlistitem> + <richlistitem id="listitem2"><label value="item2"/></richlistitem> + </richlistbox> + + <vbox id="eventdump"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/formimage.png b/accessible/tests/mochitest/formimage.png Binary files differnew file mode 100644 index 0000000000..10e44bf920 --- /dev/null +++ b/accessible/tests/mochitest/formimage.png diff --git a/accessible/tests/mochitest/grid.js b/accessible/tests/mochitest/grid.js new file mode 100644 index 0000000000..4a2e01ee9b --- /dev/null +++ b/accessible/tests/mochitest/grid.js @@ -0,0 +1,142 @@ +/* import-globals-from common.js */ + +/** + * Create grid object based on HTML table. + */ +function grid(aTableIdentifier) { + this.getRowCount = function getRowCount() { + return this.table.rows.length - (this.table.tHead ? 1 : 0); + }; + this.getColsCount = function getColsCount() { + return this.table.rows[0].cells.length; + }; + + this.getRowAtIndex = function getRowAtIndex(aIndex) { + return this.table.rows[this.table.tHead ? aIndex + 1 : aIndex]; + }; + + this.getMaxIndex = function getMaxIndex() { + return this.getRowCount() * this.getColsCount() - 1; + }; + + this.getCellAtIndex = function getCellAtIndex(aIndex) { + var colsCount = this.getColsCount(); + + var rowIdx = Math.floor(aIndex / colsCount); + var colIdx = aIndex % colsCount; + + var row = this.getRowAtIndex(rowIdx); + return row.cells[colIdx]; + }; + + this.getIndexByCell = function getIndexByCell(aCell) { + var colIdx = aCell.cellIndex; + + var rowIdx = aCell.parentNode.rowIndex; + if (this.table.tHead) { + rowIdx -= 1; + } + + var colsCount = this.getColsCount(); + return rowIdx * colsCount + colIdx; + }; + + this.getCurrentCell = function getCurrentCell() { + var rowCount = this.table.rows.length; + var colsCount = this.getColsCount(); + for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) { + for (var colIdx = 0; colIdx < colsCount; colIdx++) { + var cell = this.table.rows[rowIdx].cells[colIdx]; + if (cell.hasAttribute("tabindex")) { + return cell; + } + } + } + return null; + }; + + this.initGrid = function initGrid() { + this.table.addEventListener("keypress", this); + this.table.addEventListener("click", this); + }; + + this.handleEvent = function handleEvent(aEvent) { + if (aEvent instanceof KeyboardEvent) { + this.handleKeyEvent(aEvent); + } else { + this.handleClickEvent(aEvent); + } + }; + + this.handleKeyEvent = function handleKeyEvent(aEvent) { + if (aEvent.target.localName != "td") { + return; + } + + var cell = aEvent.target; + switch (aEvent.keyCode) { + case KeyboardEvent.DOM_VK_UP: { + let colsCount = this.getColsCount(); + let idx = this.getIndexByCell(cell); + var upidx = idx - colsCount; + if (upidx >= 0) { + cell.removeAttribute("tabindex"); + var upcell = this.getCellAtIndex(upidx); + upcell.setAttribute("tabindex", "0"); + upcell.focus(); + } + break; + } + case KeyboardEvent.DOM_VK_DOWN: { + let colsCount = this.getColsCount(); + let idx = this.getIndexByCell(cell); + var downidx = idx + colsCount; + if (downidx <= this.getMaxIndex()) { + cell.removeAttribute("tabindex"); + var downcell = this.getCellAtIndex(downidx); + downcell.setAttribute("tabindex", "0"); + downcell.focus(); + } + break; + } + case KeyboardEvent.DOM_VK_LEFT: { + let idx = this.getIndexByCell(cell); + if (idx > 0) { + cell.removeAttribute("tabindex"); + var prevcell = this.getCellAtIndex(idx - 1); + prevcell.setAttribute("tabindex", "0"); + prevcell.focus(); + } + break; + } + case KeyboardEvent.DOM_VK_RIGHT: { + let idx = this.getIndexByCell(cell); + if (idx < this.getMaxIndex()) { + cell.removeAttribute("tabindex"); + var nextcell = this.getCellAtIndex(idx + 1); + nextcell.setAttribute("tabindex", "0"); + nextcell.focus(); + } + break; + } + } + }; + + this.handleClickEvent = function handleClickEvent(aEvent) { + if (aEvent.target.localName != "td") { + return; + } + + var curCell = this.getCurrentCell(); + var cell = aEvent.target; + + if (cell != curCell) { + curCell.removeAttribute("tabindex"); + cell.setAttribute("tabindex", "0"); + cell.focus(); + } + }; + + this.table = getNode(aTableIdentifier); + this.initGrid(); +} diff --git a/accessible/tests/mochitest/hittest/a11y.ini b/accessible/tests/mochitest/hittest/a11y.ini new file mode 100644 index 0000000000..5e17a4f896 --- /dev/null +++ b/accessible/tests/mochitest/hittest/a11y.ini @@ -0,0 +1,15 @@ +[DEFAULT] +support-files = zoom_tree.xhtml + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + +[test_browser.html] +[test_canvas_hitregion.html] +skip-if = (os == "android") +[test_general.html] +[test_menu.xhtml] +[test_shadowroot.html] +support-files = test_shadowroot_subframe.html +[test_zoom.html] +[test_zoom_text.html] +[test_zoom_tree.xhtml] diff --git a/accessible/tests/mochitest/hittest/test_browser.html b/accessible/tests/mochitest/hittest/test_browser.html new file mode 100644 index 0000000000..c14df7d736 --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_browser.html @@ -0,0 +1,61 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessible::childAtPoint() from browser tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + + <script type="application/javascript"> + function doTest() { + // Hit testing. See bug #726097 + getNode("hittest").scrollIntoView(true); + + var hititem = getAccessible("hititem"); + var hittest = getAccessible("hittest"); + + var [hitX, hitY, hitWidth, hitHeight] = getBounds(hititem); + var tgtX = hitX + hitWidth / 2; + var tgtY = hitY + hitHeight / 2; + + var rootAcc = getRootAccessible(); + var docAcc = getAccessible(document); + var outerDocAcc = docAcc.parent; + + var hitAcc = rootAcc.getDeepestChildAtPoint(tgtX, tgtY); + is(hitAcc, hititem, "Hit match at " + tgtX + "," + tgtY + + ". Found: " + prettyName(hitAcc)); + var hitAcc2 = docAcc.getDeepestChildAtPoint(tgtX, tgtY); + is(hitAcc, hitAcc2, "Hit match at " + tgtX + "," + tgtY + + ". Found: " + prettyName(hitAcc2)); + + hitAcc = outerDocAcc.getChildAtPoint(tgtX, tgtY); + is(hitAcc, docAcc, "Hit match at " + tgtX + "," + tgtY + + ". Found: " + prettyName(hitAcc)); + hitAcc = docAcc.getChildAtPoint(tgtX, tgtY); + is(hitAcc, hittest, "Hit match at " + tgtX + "," + tgtY + + ". Found: " + prettyName(hitAcc)); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=726097" + title="nsIAccessible::childAtPoint() from browser tests">Mozilla Bug 726097</a> + + <div id="hittest"> + <div id="hititem"><span role="image">img</span>item</div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/hittest/test_canvas_hitregion.html b/accessible/tests/mochitest/hittest/test_canvas_hitregion.html new file mode 100644 index 0000000000..f47f1e3bc0 --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_canvas_hitregion.html @@ -0,0 +1,85 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessible::childAtPoint() for canvas from browser tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + + <script type="application/javascript"> + function redrawCheckbox(context, element, x, y) { + context.save(); + context.font = "10px sans-serif"; + context.textAlign = "left"; + context.textBaseline = "middle"; + var 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 (document.activeElement == element) + context.drawFocusIfNeeded(element); + context.addHitRegion({control: element}); + context.restore(); + } + + function doTest() { + var offsetX = 20, offsetY = 40; + getNode("hitcanvas").scrollIntoView(true); + + var context = document.getElementById("hitcanvas").getContext("2d"); + redrawCheckbox(context, document.getElementById("hitcheck"), + offsetX, offsetY); + + var hitcanvas = getAccessible("hitcanvas"); + var hitcheck = getAccessible("hitcheck"); + + var [hitX, hitY /* hitWidth, hitHeight */] = getBounds(hitcanvas); + var [deltaX, deltaY] = CSSToDevicePixels(window, offsetX, offsetY); + + var docAcc = getAccessible(document); + + // test if we hit the region associated with the shadow dom checkbox + var tgtX = hitX + deltaX; + var tgtY = hitY + deltaY; + let hitAcc = docAcc.getDeepestChildAtPoint(tgtX, tgtY); + isObject(hitAcc, hitcheck, `Hit match at (${tgtX}, ${tgtY}`); + + // test that we don't hit the region associated with the shadow dom checkbox + tgtY = hitY + deltaY * 2; + hitAcc = docAcc.getDeepestChildAtPoint(tgtX, tgtY); + isObject(hitAcc, hitcanvas, `Hit match at (${tgtX}, ${tgtY}`); + + SimpleTest.finish(); + } + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(function() { + SpecialPowers.pushPrefEnv({"set": [["canvas.hitregions.enabled", true]]}, doTest); + }); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=966591" + title="nsIAccessible::childAtPoint() for canvas hit regions from browser tests">Mozilla Bug 966591</a> + + <canvas id="hitcanvas"> + <input id="hitcheck" type="checkbox"><label for="showA"> Show A </label> + </canvas> +</body> +</html> diff --git a/accessible/tests/mochitest/hittest/test_general.html b/accessible/tests/mochitest/hittest/test_general.html new file mode 100644 index 0000000000..f5afd18446 --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_general.html @@ -0,0 +1,110 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessible::childAtPoint() tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function doPreTest() { + waitForImageMap("imgmap", doTest); + } + + function doTest() { + // Not specific case, child and deepchild testing. + var list = getAccessible("list"); + var listitem = getAccessible("listitem"); + var image = getAccessible("image"); +if (!MAC) { + testChildAtPoint(list, 1, 1, listitem, image.firstChild); +} else { + todo(false, "Bug 746974 - children must match on all platforms, disable failing test on Mac"); +} + + // ::MustPrune case (in this case childAtPoint doesn't look inside a + // textbox), point is inside of textbox. + var txt = getAccessible("txt"); + testChildAtPoint(txt, 1, 1, txt, txt); + + // ::MustPrune case, point is outside of textbox accessible but is in + // document. + testChildAtPoint(txt, -1, 1, null, null); + + // ::MustPrune case, point is outside of root accessible. + testChildAtPoint(txt, -10000, 10000, null, null); + + // Not specific case, point is inside of btn accessible. + var btn = getAccessible("btn"); + testChildAtPoint(btn, 1, 1, btn, btn); + + // Not specific case, point is outside of btn accessible. + testChildAtPoint(btn, -1, 1, null, null); + + // Out of flow accessible testing, do not return out of flow accessible + // because it's not a child of the accessible even visually it is. + var rectArea = getNode("area").getBoundingClientRect(); + var outOfFlow = getNode("outofflow"); + outOfFlow.style.left = rectArea.left + "px"; + outOfFlow.style.top = rectArea.top + "px"; + + testChildAtPoint("area", 1, 1, "area", "area"); + + // Test image maps. Their children are not in the layout tree. + var theLetterA = getAccessible("imgmap").firstChild; + hitTest("imgmap", theLetterA, theLetterA); + hitTest("container", "imgmap", theLetterA); + + // hit testing for element contained by zero-width element + hitTest("container2", "container2_input", "container2_input"); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doPreTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=491657" + title="nsIAccessible::childAtPoint() tests">Mozilla Bug 491657</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="list" id="list"> + <div role="listitem" id="listitem"><span role="image" id="image">img</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 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="../letters.gif"/> + </div> + + <div id="container2" style="width: 0px"> + <input id="container2_input"> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/hittest/test_menu.xhtml b/accessible/tests/mochitest/hittest/test_menu.xhtml new file mode 100644 index 0000000000..d80b31305d --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_menu.xhtml @@ -0,0 +1,133 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Hit testing for XUL menus"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../layout.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + function openMenu(aMenuID, aMenuPopupID, aMenuItemID) + { + this.menuNode = getNode(aMenuID); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, this.menuNode) + ]; + + this.invoke = function openMenu_invoke() + { + this.menuNode.open = true; + } + + this.finalCheck = function openMenu_finalCheck() + { + hitTest(aMenuPopupID, aMenuItemID, aMenuItemID); + } + + this.getID = function openMenu_invoke() + { + return "open menu '" + aMenuID + "' and do hit testing"; + } + } + + function closeMenu(aID, aSubID, aSub2ID) + { + this.menuNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, document) + ]; + + this.invoke = function openMenu_invoke() + { + this.menuNode.open = false; + } + + this.finalCheck = function openMenu_finalCheck() + { + testStates(aID, 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + testStates(aSubID, STATE_INVISIBLE, 0, STATE_OFFSCREEN); + testStates(aSub2ID, STATE_INVISIBLE, 0, STATE_OFFSCREEN); + } + + this.getID = function openMenu_invoke() + { + return "open menu and test states"; + } + } + + var gQueue = null; + function doTest() + { + if (LINUX) { + ok(true, "No tests is running on Linux"); + SimpleTest.finish(); + return; + } + + getNode("mi_file1").scrollIntoView(true); + + gQueue = new eventQueue(); + gQueue.push(new openMenu("mi_file1", "mp_file1", "mi_file1.1")); + gQueue.push(new openMenu("mi_file1.2", "mp_file1.2", "mi_file1.2.1")); + gQueue.push(new closeMenu("mi_file1", "mi_file1.1", "mi_file1.2.1")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=670087" + title="AccessibleObjectFromPoint returns incorrect accessible for popup menus"> + Bug 670087 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + + <menubar> + <menu label="File" id="mi_file1"> + <menupopup id="mp_file1"> + <menuitem label="SubFile" id="mi_file1.1"/> + <menu label="SubFile2" id="mi_file1.2"> + <menupopup style="max-height: 5em;" id="mp_file1.2"> + <menuitem label="SubSubFile" id="mi_file1.2.1"/> + <menuitem label="SubSubFile2" id="mi_file1.2.2"/> + <menuitem label="SubSubFile3" id="mi_file1.2.3"/> + <menuitem label="SubSubFile4" id="mi_file1.2.4"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menubar> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/hittest/test_shadowroot.html b/accessible/tests/mochitest/hittest/test_shadowroot.html new file mode 100644 index 0000000000..6acdc47987 --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_shadowroot.html @@ -0,0 +1,35 @@ +<!DOCTYPE html> +<html> +<head> + <title>ShadowRoot hit tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +</head> +<body> + + <a target="_blank" + title="Test getChildAtPoint works for shadow DOM content" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1027315"> + Mozilla Bug 1027315 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <script> + SimpleTest.waitForExplicitFinish(); + + window.onload = () => { + var iframe = document.createElement("iframe"); + iframe.src = "test_shadowroot_subframe.html"; + document.body.appendChild(iframe); + }; + + </script> + +</body> +</html> diff --git a/accessible/tests/mochitest/hittest/test_shadowroot_subframe.html b/accessible/tests/mochitest/hittest/test_shadowroot_subframe.html new file mode 100644 index 0000000000..7a365e7b93 --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_shadowroot_subframe.html @@ -0,0 +1,58 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>ShadowRoot hit tests</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="../common.js"></script> + <script type="application/javascript" src="../layout.js"></script> + + <script type="application/javascript"> + let SimpleTest = window.parent.SimpleTest; + let ok = window.parent.ok; + let is = window.parent.is; + + function doTest() { + var componentAcc = getAccessible("component1"); + testChildAtPoint(componentAcc, 1, 1, componentAcc.firstChild, + componentAcc.firstChild); + + componentAcc = getAccessible("component2"); + testChildAtPoint(componentAcc, 1, 1, componentAcc.firstChild, + componentAcc.firstChild); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> +<body> + <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> +</body> +</html> diff --git a/accessible/tests/mochitest/hittest/test_zoom.html b/accessible/tests/mochitest/hittest/test_zoom.html new file mode 100644 index 0000000000..70e71f7a6d --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_zoom.html @@ -0,0 +1,59 @@ +<!DOCTYPE html> +<html> +<head> + <title>childAtPoint when page is zoomed</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + function doTest() { +if (!MAC) { + var tabDocument = currentTabDocument(); + var p1 = tabDocument.body.firstElementChild; + var p2 = tabDocument.body.lastElementChild; + + hitTest(tabDocument, p1, p1.firstChild); + hitTest(tabDocument, p2, p2.firstChild); + + zoomDocument(tabDocument, 2.0); + + hitTest(tabDocument, p1, p1.firstChild); + hitTest(tabDocument, p2, p2.firstChild); + + closeBrowserWindow(); +} else { + todo(false, "Bug 746974 - deepest child must be correct on all platforms, disabling on Mac!"); +} + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTest, + "data:text/html,<html><body><p>para 1</p><p>para 2</p></body></html>", + { left: 100, top: 100 }); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=727942" + title="childAtPoint may return incorrect accessibles when page zoomed"> + Mozilla Bug 727942 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/hittest/test_zoom_text.html b/accessible/tests/mochitest/hittest/test_zoom_text.html new file mode 100644 index 0000000000..4dc92b9639 --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_zoom_text.html @@ -0,0 +1,57 @@ +<!DOCTYPE html> +<html> +<head> + <title>getOffsetAtPoint when page is zoomed</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + + <script type="application/javascript"> + function doTest() { + var hyperText = getNode("paragraph"); + var textNode = hyperText.firstChild; + let [x, y, width, height] = getBounds(textNode); + testOffsetAtPoint(hyperText, x + width / 2, y + height / 2, + COORDTYPE_SCREEN_RELATIVE, + hyperText.textContent.length / 2); + + zoomDocument(document, 2.0); + + document.body.offsetTop; // getBounds doesn't flush layout on its own, looks like. + + [x, y, width, height] = getBounds(textNode); + testOffsetAtPoint(hyperText, x + width / 2, y + height / 2, + COORDTYPE_SCREEN_RELATIVE, + hyperText.textContent.length / 2); + + zoomDocument(document, 1.0); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=727942" + title="getOffsetAtPoint returns incorrect value when page is zoomed"> + Mozilla Bug 727942 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <p id="paragraph" style="font-family: monospace;">Болтали две сороки</p> +</body> +</html> diff --git a/accessible/tests/mochitest/hittest/test_zoom_tree.xhtml b/accessible/tests/mochitest/hittest/test_zoom_tree.xhtml new file mode 100644 index 0000000000..54cb37c871 --- /dev/null +++ b/accessible/tests/mochitest/hittest/test_zoom_tree.xhtml @@ -0,0 +1,97 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="nsIAccessible::getChildAtPoint and getDeepestChildAtPoint"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/chrome-harness.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../layout.js" /> + <script type="application/javascript" + src="../browser.js" /> + + <script type="application/javascript"> + <![CDATA[ + function doTest() + { + var tabDocument = currentTabDocument(); + var tabWindow = currentTabWindow(); + + var tree = tabDocument.getElementById("tree"); + var treecols = tabDocument.getElementById("treecols"); + var treecol1 = tabDocument.getElementById("treecol1"); + + // tree columns + hitTest(tree, treecols, treecol1); + + // tree rows and cells + var treeRect = tree.treeBody.getBoundingClientRect(); + var rect = tree.getCoordsForCellItem(1, tree.columns[0], "cell"); + + var treeAcc = getAccessible(tree, [nsIAccessibleTable]); + var cellAcc = treeAcc.getCellAt(1, 0); + var rowAcc = cellAcc.parent; + + var cssX = rect.x + treeRect.x; + var cssY = rect.y + treeRect.y; + var [x, y] = CSSToDevicePixels(tabWindow, cssX, cssY); + + testChildAtPoint(treeAcc, x, y, rowAcc, cellAcc); + testChildAtPoint(rowAcc, x, y, cellAcc, cellAcc); + + // do zoom + zoomDocument(tabDocument, 1.5); + + // tree columns + hitTest(tree, treecols, treecol1); + + // tree rows and cells + [x, y] = CSSToDevicePixels(tabWindow, cssX, cssY); + testChildAtPoint(treeAcc, x, y, rowAcc, cellAcc); + testChildAtPoint(rowAcc, x, y, cellAcc, cellAcc); + + closeBrowserWindow(); + SimpleTest.finish(); + } + + function prepareTest() + { + var tabDocument = currentTabDocument(); + var tree = tabDocument.getElementById("tree"); + loadXULTreeAndDoTest(doTest, tree, new nsTableTreeView(5)); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(prepareTest, + getRootDirectory(window.location.href) + "zoom_tree.xhtml", + { left: 100, top: 100 }); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=471493" + title=" crash [@ nsPropertyTable::GetPropertyInternal]"> + Mozilla Bug 471493 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/hittest/zoom_tree.xhtml b/accessible/tests/mochitest/hittest/zoom_tree.xhtml new file mode 100644 index 0000000000..52ec0932ab --- /dev/null +++ b/accessible/tests/mochitest/hittest/zoom_tree.xhtml @@ -0,0 +1,18 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="nsIAccessible::getChildAtPoint and getDeepestChildAtPoint for XUL trees"> + + <tree id="tree" flex="1"> + <treecols id="treecols"> + <treecol id="treecol1" flex="1" primary="true" label="column"/> + <treecol id="treecol2" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + +</window> + diff --git a/accessible/tests/mochitest/hyperlink/a11y.ini b/accessible/tests/mochitest/hyperlink/a11y.ini new file mode 100644 index 0000000000..60804a70c8 --- /dev/null +++ b/accessible/tests/mochitest/hyperlink/a11y.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = hyperlink.js + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + +[test_general.html] +[test_general.xhtml] diff --git a/accessible/tests/mochitest/hyperlink/hyperlink.js b/accessible/tests/mochitest/hyperlink/hyperlink.js new file mode 100644 index 0000000000..93caa9090c --- /dev/null +++ b/accessible/tests/mochitest/hyperlink/hyperlink.js @@ -0,0 +1,46 @@ +/* import-globals-from ../common.js */ +/* import-globals-from ../events.js */ +/* import-globals-from ../states.js */ + +/** + * Focus hyperlink invoker. + * + * @param aID [in] hyperlink identifier + * @param aSelectedAfter [in] specifies if hyperlink is selected/focused after + * the focus + */ +function focusLink(aID, aSelectedAfter) { + this.node = getNode(aID); + this.accessible = getAccessible(this.node); + + this.eventSeq = []; + this.unexpectedEventSeq = []; + + var checker = new invokerChecker(EVENT_FOCUS, this.accessible); + if (aSelectedAfter) { + this.eventSeq.push(checker); + } else { + this.unexpectedEventSeq.push(checker); + } + + this.invoke = function focusLink_invoke() { + var expectedStates = aSelectedAfter ? STATE_FOCUSABLE : 0; + var unexpectedStates = + (!aSelectedAfter ? STATE_FOCUSABLE : 0) | STATE_FOCUSED; + testStates(aID, expectedStates, 0, unexpectedStates, 0); + + this.node.focus(); + }; + + this.finalCheck = function focusLink_finalCheck() { + var expectedStates = aSelectedAfter ? STATE_FOCUSABLE | STATE_FOCUSED : 0; + var unexpectedStates = !aSelectedAfter + ? STATE_FOCUSABLE | STATE_FOCUSED + : 0; + testStates(aID, expectedStates, 0, unexpectedStates, 0); + }; + + this.getID = function focusLink_getID() { + return "focus hyperlink " + prettyName(aID); + }; +} diff --git a/accessible/tests/mochitest/hyperlink/test_general.html b/accessible/tests/mochitest/hyperlink/test_general.html new file mode 100644 index 0000000000..236b44dac4 --- /dev/null +++ b/accessible/tests/mochitest/hyperlink/test_general.html @@ -0,0 +1,273 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=418368 +--> +<head> + <title>nsIHyperLinkAccessible chrome tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript" + src="hyperlink.js"></script> + + <script type="application/javascript"> + function testThis(aID, aAcc, aRole, aAnchors, aName, aValid, aStartIndex, + aEndIndex) { + testRole(aAcc, aRole); + is(aAcc.anchorCount, aAnchors, "Wrong number of anchors for ID " + + aID + "!"); + is(aAcc.getAnchor(0).name, aName, "Wrong name for ID " + + aID + "!"); + is(aAcc.valid, aValid, "No correct valid state for ID " + + aID + "!"); + is(aAcc.startIndex, aStartIndex, "Wrong startIndex value for ID " + + aID + "!"); + is(aAcc.endIndex, aEndIndex, "Wrong endIndex value for ID " + + aID + "!"); + } + + function testAction(aId, aAcc, aActionName) { + var actionCount = aActionName ? 1 : 0; + is(aAcc.actionCount, actionCount, + "Wrong actions number for ID " + aId); + try { + is(aAcc.getActionName(0), aActionName, + "Wrong action name for ID " + aId); + } catch (e) { + if (actionCount) + ok(false, "Exception on action name getting for ID " + aId); + else + ok(true, "Correct action name for ID " + aId); + } + } + + // gA11yEventDumpToConsole = true; // debug stuff + function doPreTest() { + waitForImageMap("imgmap", doTest); + } + + var gQueue = null; + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // normal hyperlink + var normalHyperlinkAcc = getAccessible("NormalHyperlink", + [nsIAccessibleHyperLink]); + testThis("NormalHyperlink", normalHyperlinkAcc, ROLE_LINK, 1, + "Mozilla Foundation", true, 17, 18); + is(normalHyperlinkAcc.getURI(0).spec, "http://www.mozilla.org/", + "URI wrong for normalHyperlinkElement!"); + testStates(normalHyperlinkAcc, STATE_LINKED, 0); + + // //////////////////////////////////////////////////////////////////////// + // ARIA hyperlink + var ariaHyperlinkAcc = getAccessible("AriaHyperlink", + [nsIAccessibleHyperLink]); + testThis("AriaHyperlink", ariaHyperlinkAcc, ROLE_LINK, 1, + "Mozilla Foundation Home", true, 30, 31); + testStates(ariaHyperlinkAcc, STATE_LINKED, 0); + testAction("AriaHyperlink", ariaHyperlinkAcc, "click"); + + // //////////////////////////////////////////////////////////////////////// + // ARIA hyperlink with status invalid + var invalidAriaHyperlinkAcc = getAccessible("InvalidAriaHyperlink", + [nsIAccessibleHyperLink]); + is(invalidAriaHyperlinkAcc.valid, false, "Should not be valid!"); + testStates(invalidAriaHyperlinkAcc, STATE_LINKED, 0); + + // //////////////////////////////////////////////////////////////////////// + // image map and its link children + + var imageMapHyperlinkAcc = getAccessible("imgmap", + [nsIAccessibleHyperLink]); + testThis("imgmap", imageMapHyperlinkAcc, ROLE_IMAGE_MAP, 2, "b", true, + 79, 80); + is(imageMapHyperlinkAcc.getURI(0).spec, + "http://www.bbc.co.uk/radio4/atoz/index.shtml#b", "URI wrong!"); + is(imageMapHyperlinkAcc.getURI(1).spec, + "http://www.bbc.co.uk/radio4/atoz/index.shtml#a", "URI wrong!"); + testStates(imageMapHyperlinkAcc, 0, 0); + + var area1 = getAccessible(imageMapHyperlinkAcc.firstChild, + [nsIAccessibleHyperLink]); + testThis("Area1", area1, ROLE_LINK, 1, "b", true, 0, 1); + is(area1.getURI(0).spec, + "http://www.bbc.co.uk/radio4/atoz/index.shtml#b", "URI wrong!"); + testStates(area1, (STATE_LINKED)); + + var area2 = getAccessible(area1.nextSibling, + [nsIAccessibleHyperLink]); + testThis("Area2", area2, ROLE_LINK, 1, "a", true, 1, 2); + is(area2.getURI(0).spec, + "http://www.bbc.co.uk/radio4/atoz/index.shtml#a", "URI wrong!"); + testStates(area2, (STATE_LINKED)); + + // //////////////////////////////////////////////////////////////////////// + // empty hyperlink + var EmptyHLAcc = getAccessible("emptyLink", + [nsIAccessibleHyperLink]); + testThis("emptyLink", EmptyHLAcc, ROLE_LINK, 1, null, true, 93, 94); + testStates(EmptyHLAcc, (STATE_FOCUSABLE | STATE_LINKED), 0); + testAction("emptyLink", EmptyHLAcc, "jump"); + + // //////////////////////////////////////////////////////////////////////// + // normal hyperlink with embedded span + var hyperlinkWithSpanAcc = getAccessible("LinkWithSpan", + [nsIAccessibleHyperLink]); + testThis("LinkWithSpan", hyperlinkWithSpanAcc, ROLE_LINK, 1, + "Heise Online", true, 119, 120); + is(hyperlinkWithSpanAcc.getURI(0).spec, "http://www.heise.de/", + "URI wrong for hyperlinkElementWithSpan!"); + testStates(hyperlinkWithSpanAcc, STATE_LINKED, 0); + testAction("LinkWithSpan", hyperlinkWithSpanAcc, "jump"); + + // //////////////////////////////////////////////////////////////////////// + // Named anchor, should never have state_linked + var namedAnchorAcc = getAccessible("namedAnchor", + [nsIAccessibleHyperLink]); + testThis("namedAnchor", namedAnchorAcc, ROLE_LINK, 1, + "This should never be of state_linked", true, 196, 197); + testStates(namedAnchorAcc, STATE_SELECTABLE, + 0, (STATE_FOCUSABLE | STATE_LINKED)); + testAction("namedAnchor", namedAnchorAcc, ""); + + // //////////////////////////////////////////////////////////////////////// + // No link (hasn't any attribute), should never have state_linked + var noLinkAcc = getAccessible("noLink", + [nsIAccessibleHyperLink]); + testThis("noLink", noLinkAcc, ROLE_LINK, 1, + "This should never be of state_linked", true, 254, 255); + testStates(noLinkAcc, 0, 0, (STATE_FOCUSABLE | STATE_LINKED)); + testAction("noLink", noLinkAcc, ""); + + // //////////////////////////////////////////////////////////////////////// + // Link with registered 'click' event, should have state_linked + var linkWithClickAcc = getAccessible("linkWithClick", + [nsIAccessibleHyperLink]); + testThis("linkWithClick", linkWithClickAcc, ROLE_LINK, 1, + "This should have state_linked", true, 292, 293); + testStates(linkWithClickAcc, STATE_LINKED, 0); + testAction("linkWithClick", linkWithClickAcc, "click"); + + // //////////////////////////////////////////////////////////////////////// + // Maps to group links (bug 431615). + // var linksMapAcc = getAccessible("linksmap"); + + // //////////////////////////////////////////////////////////////////////// + // Link with title attribute, no name from the subtree (bug 438325). + var id = "linkWithTitleNoNameFromSubtree"; + var linkAcc = getAccessible(id, [nsIAccessibleHyperLink]); + testThis(id, linkAcc, ROLE_LINK, 1, "Link with title", true, 344, 345); + testStates(linkAcc, STATE_LINKED, 0); + testAction(id, linkAcc, "jump"); + + // //////////////////////////////////////////////////////////////////////// + // Link with title attribute, name from the subtree - onscreen name + // (bug 438325). + id = "linkWithTitleNameFromSubtree"; + linkAcc = getAccessible(id, [nsIAccessibleHyperLink]); + testThis(id, linkAcc, ROLE_LINK, 1, "the name from subtree", true, 393, + 394); + testStates(linkAcc, STATE_LINKED, 0); + testAction(id, linkAcc, "jump"); + + // //////////////////////////////////////////////////////////////////////// + // Link with title attribute, name from the nested html:img (bug 438325). + id = "linkWithTitleNameFromImg"; + linkAcc = getAccessible(id, [nsIAccessibleHyperLink]); + testThis(id, linkAcc, ROLE_LINK, 1, "The title for link", true, 447, + 448); + testStates(linkAcc, STATE_LINKED, 0); + testAction(id, linkAcc, "jump"); + + // //////////////////////////////////////////////////////////////////////// + // Text accessible shouldn't implement nsIAccessibleHyperLink + var res = isAccessible(getNode("namedAnchor").firstChild, + [nsIAccessibleHyperLink]); + ok(!res, "Text accessible shouldn't implement nsIAccessibleHyperLink"); + + // //////////////////////////////////////////////////////////////////////// + // Test focus + gQueue = new eventQueue(); + + gQueue.push(new focusLink("NormalHyperlink", true)); + gQueue.push(new focusLink("AriaHyperlink", true)); + gQueue.push(new focusLink("InvalidAriaHyperlink", false)); + gQueue.push(new focusLink("LinkWithSpan", true)); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doPreTest); + </script> + +</head> +<body><a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=418368">Mozilla Bug 418368</a + ><p id="display"></p + ><div id="content" style="display: none"></div + ><pre id="test"> + </pre + ><br + >Simple link:<br + ><a id="NormalHyperlink" href="http://www.mozilla.org">Mozilla Foundation</a + ><br>ARIA link:<br + ><span id="AriaHyperlink" role="link" + onclick="window.open('http://www.mozilla.org/');" + tabindex="0">Mozilla Foundation Home</span + ><br + >Invalid, non-focusable hyperlink:<br + ><span id="InvalidAriaHyperlink" role="link" aria-invalid="true" + onclick="window.open('http:/www.mozilla.org/');">Invalid link</span + ><br>Image map:<br + ><map name="atoz_map" + ><area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" + alt="b" + shape="rect"></area + ><area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a" + coords="0,0,13,14" + alt="a" + shape="rect"></area + ></map + ><img width="447" id="imgmap" + height="15" + usemap="#atoz_map" + src="../letters.gif"><br>Empty link:<br + ><a id="emptyLink" href=""><img src=""></a + ><br>Link with embedded span<br + ><a id="LinkWithSpan" href="http://www.heise.de/"><span lang="de">Heise Online</span></a + ><br>Named anchor, must not have "linked" state for it to be exposed correctly:<br + ><a id="namedAnchor" name="named_anchor">This should never be of state_linked</a + ><br>Link having no attributes, must not have "linked" state:<a id="noLink" + >This should never be of state_linked</a + ><br>Link with registered 'click' event: <a id="linkWithClick" onclick="var clicked = true;" + >This should have state_linked</a + ><br>Link with title attribute (no name from subtree): <a + id="linkWithTitleNoNameFromSubtree" href="http://www.heise.de/" + title="Link with title"><img src=""/></a + ><br>Link with title attribute (name from subtree): <a + id="linkWithTitleNameFromSubtree" href="http://www.heise.de/" + title="Link with title">the name from subtree</a + ><br>Link with title attribute (name from nested image): <a + id="linkWithTitleNameFromImg" href="http://www.heise.de/" + title="Link with title"><img src="" alt="The title for link"/></a + ><br><br>Map that is used to group links (www.w3.org/TR/WCAG10-HTML-TECHS/#group-bypass), also see the bug 431615:<br + ><map id="linksmap" title="Site navigation"><ul + ><li><a href="http://mozilla.org">About the project</a></li + ><li><a href="http://mozilla.org">Sites and sounds</a></li + ></ul + ></map +></body> +</html> diff --git a/accessible/tests/mochitest/hyperlink/test_general.xhtml b/accessible/tests/mochitest/hyperlink/test_general.xhtml new file mode 100644 index 0000000000..b7ce2add31 --- /dev/null +++ b/accessible/tests/mochitest/hyperlink/test_general.xhtml @@ -0,0 +1,96 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="test for nsIAccessibleHyperLink interface on XUL:label elements"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript" + src="hyperlink.js" /> + + <script type="application/javascript"> + <![CDATA[ + function testThis(aID, aAcc, aRole, aAnchorCount, aAnchorName, aURI, + aStartIndex, aEndIndex, aValid) + { + testRole(aID, aRole); + is(aAcc.anchorCount, aAnchorCount, "Wrong number of anchors for ID " + + aID + "!"); + is(aAcc.getAnchor(0).name, aAnchorName, "Wrong name for ID " + aID + "!"); + is(aAcc.getURI(0).spec, aURI, "URI wrong for ID " + aID + "!"); + is(aAcc.startIndex, aStartIndex, "Wrong startIndex value for ID " + aID + + "!"); + is(aAcc.endIndex, aEndIndex, "Wrong endIndex value for ID " + aID + "!"); + is(aAcc.valid, aValid, "Wrong valid state for ID " + aID + "!"); + } + + var gQueue = null; + function doTest() + { + var linkedLabelAcc = getAccessible("linkedLabel", + [nsIAccessibleHyperLink]); + testThis("linkedLabel", linkedLabelAcc, ROLE_LINK, 1, + "Mozilla Foundation home", "http://www.mozilla.org/", 1, 2, + true); + testStates(linkedLabelAcc, STATE_LINKED, 0); + + var labelWithValueAcc = getAccessible("linkLabelWithValue", + [nsIAccessibleHyperLink]); + testThis("linkLabelWithValue", labelWithValueAcc, ROLE_LINK, 1, + "Mozilla Foundation", "http://www.mozilla.org/", 2, 3, true, + false, true); + testStates(labelWithValueAcc, STATE_LINKED, EXT_STATE_HORIZONTAL); + + var normalLabelAcc = getAccessible("normalLabel"); + testRole(normalLabelAcc, ROLE_LABEL); + is(normalLabelAcc.name, "This label should not be a link", + "Wrong name for normal label!"); + testStates(normalLabelAcc, 0, 0, (STATE_FOCUSABLE | STATE_LINKED)); + + ////////////////////////////////////////////////////////////////////////// + // Test focus + + gQueue = new eventQueue(); + + gQueue.push(new focusLink("linkedLabel", true)); + gQueue.push(new focusLink("linkLabelWithValue", true)); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=421066" + title="Implement Mochitests for the nsIAccessibleHyperLink interface on XUL:label elements"> + Mozilla Bug 421066 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <label id="linkedLabel" href="http://www.mozilla.org/" is="text-link"> + Mozilla Foundation home</label> + <label id="linkLabelWithValue" value="Mozilla Foundation" is="text-link" + href="http://www.mozilla.org/" /> + <label id="normalLabel" value="This label should not be a link" /> +</window> diff --git a/accessible/tests/mochitest/hypertext/a11y.ini b/accessible/tests/mochitest/hypertext/a11y.ini new file mode 100644 index 0000000000..27f878f743 --- /dev/null +++ b/accessible/tests/mochitest/hypertext/a11y.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + +[test_general.html] +[test_update.html] diff --git a/accessible/tests/mochitest/hypertext/test_general.html b/accessible/tests/mochitest/hypertext/test_general.html new file mode 100644 index 0000000000..a89be54f95 --- /dev/null +++ b/accessible/tests/mochitest/hypertext/test_general.html @@ -0,0 +1,150 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=428248 +--> +<head> + <title>nsIHyper>TextAccessible chrome tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + var gParagraphAcc; + + function testLinkIndexAtOffset(aID, aOffset, aIndex) { + var htAcc = getAccessible(aID, [nsIAccessibleHyperText]); + is(htAcc.getLinkIndexAtOffset(aOffset), aIndex, + "Wrong link index at offset " + aOffset + " for ID " + aID + "!"); + } + + function testThis(aID, aCharIndex, aExpectedLinkIndex, aName) { + testLinkIndexAtOffset(gParagraphAcc, aCharIndex, aExpectedLinkIndex); + + var linkAcc = gParagraphAcc.getLinkAt(aExpectedLinkIndex); + ok(linkAcc, "No accessible for link " + aID + "!"); + + var linkIndex = gParagraphAcc.getLinkIndex(linkAcc); + is(linkIndex, aExpectedLinkIndex, "Wrong link index for " + aID + "!"); + + // Just test the link's name to make sure we get the right one. + is(linkAcc.getAnchor(0).name, aName, "Wrong name for " + aID + "!"); + } + + // gA11yEventDumpToConsole = true; + function doPreTest() { + waitForImageMap("imgmap", doTest); + } + + function doTest() { + // Test link count + gParagraphAcc = getAccessible("testParagraph", [nsIAccessibleHyperText]); + is(gParagraphAcc.linkCount, 7, "Wrong link count for paragraph!"); + + // normal hyperlink + testThis("NormalHyperlink", 14, 0, "Mozilla Foundation"); + + // ARIA hyperlink + testThis("AriaHyperlink", 27, 1, "Mozilla Foundation Home"); + + // ARIA hyperlink with status invalid + testThis("InvalidAriaHyperlink", 63, 2, "Invalid link"); + + // image map, but not its link children. They are not part of hypertext. + testThis("imgmap", 76, 3, "b"); + + // empty hyperlink + testThis("emptyLink", 90, 4, null); + + // normal hyperlink with embedded span + testThis("LinkWithSpan", 116, 5, "Heise Online"); + + // Named anchor + testThis("namedAnchor", 193, 6, "This should never be of state_linked"); + + // Paragraph with link + var p2 = getAccessible("p2", [nsIAccessibleHyperText]); + var link = p2.getLinkAt(0); + is(link, p2.getChildAt(0), "Wrong link for p2"); + is(p2.linkCount, 1, "Wrong link count for p2"); + + // getLinkIndexAtOffset, causes the offsets to be cached; + testLinkIndexAtOffset("p4", 0, 0); // 1st 'mozilla' link + testLinkIndexAtOffset("p4", 1, 1); // 2nd 'mozilla' link + testLinkIndexAtOffset("p4", 2, -1); // ' ' of ' te' text node + testLinkIndexAtOffset("p4", 3, -1); // 't' of ' te' text node + testLinkIndexAtOffset("p4", 5, -1); // 'x' of 'xt ' text node + testLinkIndexAtOffset("p4", 7, -1); // ' ' of 'xt ' text node + testLinkIndexAtOffset("p4", 8, 2); // 3d 'mozilla' link + testLinkIndexAtOffset("p4", 9, 2); // the end, latest link + + // the second pass to make sure link indexes are calculated propertly from + // cached offsets. + testLinkIndexAtOffset("p4", 0, 0); // 1st 'mozilla' link + testLinkIndexAtOffset("p4", 1, 1); // 2nd 'mozilla' link + testLinkIndexAtOffset("p4", 2, -1); // ' ' of ' te' text node + testLinkIndexAtOffset("p4", 3, -1); // 't' of ' te' text node + testLinkIndexAtOffset("p4", 5, -1); // 'x' of 'xt ' text node + testLinkIndexAtOffset("p4", 7, -1); // ' ' of 'xt ' text node + testLinkIndexAtOffset("p4", 8, 2); // 3d 'mozilla' link + testLinkIndexAtOffset("p4", 9, 2); // the end, latest link + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doPreTest); + </script> + +</head> +<body> + + <a target="_blank" + title="Create tests for NSIAccessibleHyperlink interface" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=418368"> + Mozilla Bug 418368 + </a><br> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <p id="testParagraph"><br + >Simple link:<br + ><a id="NormalHyperlink" href="http://www.mozilla.org">Mozilla Foundation</a><br + >ARIA link:<br + ><span id="AriaHyperlink" role="link" + onclick="window.open('http://www.mozilla.org/');" + tabindex="0">Mozilla Foundation Home</span><br + >Invalid, non-focusable hyperlink:<br + ><span id="InvalidAriaHyperlink" role="link" aria-invalid="true" + onclick="window.open('http:/www.mozilla.org/');">Invalid link</span><br + >Image map:<br + ><map name="atoz_map"><area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" + alt="b" + shape="rect"></area + ><area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a" + coords="0,0,13,14" + alt="a" + shape="rect"></area></map + ><img width="447" id="imgmap" + height="15" + usemap="#atoz_map" + src="../letters.gif"></img><br + >Empty link:<br + ><a id="emptyLink" href=""><img src=""></img></a><br + >Link with embedded span<br + ><a id="LinkWithSpan" href="http://www.heise.de/"><span lang="de">Heise Online</span></a><br + >Named anchor, must not have "linked" state for it to be exposed correctly:<br + ><a id="namedAnchor" name="named_anchor">This should never be of state_linked</a> + </p> + <p id="p2"><a href="http://mozilla.org">mozilla.org</a></p> + <p id="p4"><a href="www">mozilla</a><a href="www">mozilla</a><span> te</span><span>xt </span><a href="www">mozilla</a></p> +</body> +</html> diff --git a/accessible/tests/mochitest/hypertext/test_update.html b/accessible/tests/mochitest/hypertext/test_update.html new file mode 100644 index 0000000000..f3407bea64 --- /dev/null +++ b/accessible/tests/mochitest/hypertext/test_update.html @@ -0,0 +1,214 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIHyper>TextAccessible in dynamic tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + const kLinksCount = 128; + function addLinks(aContainerID) { + this.containerNode = getNode(aContainerID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function addLinks_invoke() { + for (var jdx = 0; jdx < kLinksCount; jdx++) { + var a = document.createElement("a"); + a.setAttribute("href", "mozilla.org"); + a.textContent = "mozilla"; + this.containerNode.appendChild(a); + + var span = document.createElement("span"); + span.textContent = " text "; + this.containerNode.appendChild(span); + } + }; + + this.finalCheck = function addLinks_finalCheck() { + // getLinkAt and getLinkIndex. + var htAcc = getAccessible(this.containerNode, [nsIAccessibleHyperText]); + for (var jdx = 0; jdx < kLinksCount; jdx++) { + var link = htAcc.getLinkAt(jdx); + ok(link, "No link at index " + jdx + " for '" + aContainerID + "'"); + + var linkIdx = htAcc.getLinkIndex(link); + is(linkIdx, jdx, "Wrong link index for '" + aContainerID + "'!"); + } + }; + + this.getID = function addLinks_getID() { + return "Add links for '" + aContainerID + "'"; + }; + } + + function updateText(aContainerID) { + this.containerNode = getNode(aContainerID); + this.container = getAccessible(this.containerNode, nsIAccessibleHyperText); + this.text = this.container.firstChild; + this.textNode = this.text.DOMNode; + this.textLen = this.textNode.data.length; + + this.eventSeq = [ + new invokerChecker(EVENT_TEXT_INSERTED, this.containerNode), + ]; + + this.invoke = function updateText_invoke() { + is(this.container.getLinkIndexAtOffset(this.textLen), 0, + "Wrong intial text offsets!"); + + this.text.DOMNode.appendData(" my"); + }; + + this.finalCheck = function updateText_finalCheck() { + is(this.container.getLinkIndexAtOffset(this.textLen), -1, + "Text offsets weren't updated!"); + }; + + this.getID = function updateText_getID() { + return "update text for '" + aContainerID + "'"; + }; + } + + /** + * Text offsets must be updated when hypertext child is removed. + */ + function removeChild(aContainerID, aChildID, aInitialText, aFinalText) { + this.containerNode = getNode(aContainerID); + this.container = getAccessible(this.containerNode, nsIAccessibleText); + this.childNode = getNode(aChildID); + + // Call first to getText so offsets are cached + is(this.container.getText(0, -1), aInitialText, + "Wrong text before child removal"); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function removeChild_invoke() { + this.containerNode.removeChild(this.childNode); + }; + + this.finalCheck = function removeChild_finalCheck() { + is(this.container.getText(0, -1), aFinalText, + "Wrong text after child removal"); + is(this.container.characterCount, aFinalText.length, + "Wrong text after child removal"); + }; + + this.getID = function removeChild_getID() { + return "check text after removing child from '" + aContainerID + "'"; + }; + } + + function removeFirstChild(aContainer) { + this.ht = getAccessible(aContainer, [ nsIAccessibleHyperText ]); + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, aContainer), + ]; + + this.invoke = function removeFirstChild_invoke() { + is(this.ht.linkCount, 2, "Wrong embedded objects count before removal"); + + getNode(aContainer).removeChild(getNode(aContainer).firstElementChild); + }; + + this.finalCheck = function removeFirstChild_finalCheck() { + // check list index before link count + is(this.ht.getLinkIndex(this.ht.firstChild), 0, "Wrong child index"); + is(this.ht.linkCount, 1, "Wrong embedded objects count after removal"); + }; + + this.getID = function removeFirstChild_getID() { + return "Remove first child and check embedded object indeces"; + }; + } + + function removeLastChild(aContainer) { + this.ht = getAccessible(aContainer, [ nsIAccessibleHyperText ]); + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, aContainer), + ]; + + this.invoke = function removeLastChild_invoke() { + is(this.ht.linkCount, 1, "Wrong embedded objects count before removal"); + + getNode(aContainer).removeChild(getNode(aContainer).lastElementChild); + }; + + this.finalCheck = function removeLastChild_finalCheck() { + is(this.ht.linkCount, 0, "Wrong embedded objects count after removal"); + + var link = null; + try { + link = this.ht.getLinkAt(0); + } catch (e) { } + ok(!link, "No embedded object is expected"); + }; + + this.getID = function removeLastChild_getID() { + return "Remove last child and try its embedded object"; + }; + } + + // gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + gQueue.push(new addLinks("p1")); + gQueue.push(new updateText("p2")); + gQueue.push(new removeChild("div1", "div2", + "hello my good friend", "hello friend")); + gQueue.push(new removeFirstChild("c4")); + gQueue.push(new removeLastChild("c5")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Cache links within hypertext accessible" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=572394"> + Mozilla Bug 572394 + </a> + <a target="_blank" + title="Text offsets don't get updated when text of first child text accessible is changed" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=625009"> + Mozilla Bug 625009 + </a> + <a target="_blank" + title="Crash in nsHyperTextAccessible::GetText()" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=630841"> + Mozilla Bug 630841 + </a><br> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="p1"></p> + <p id="p2"><b>hello</b><a>friend</a></p> + <div id="div1">hello<span id="div2"> my<span id="div3"> good</span></span> friend</span></div> + <form id="c4"> + <label for="c4_input">label</label> + <input id="c4_input"> + </form> + <div id="c5">TextLeaf<input id="c5_input"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/layout.js b/accessible/tests/mochitest/layout.js new file mode 100644 index 0000000000..14e70e9288 --- /dev/null +++ b/accessible/tests/mochitest/layout.js @@ -0,0 +1,385 @@ +/* import-globals-from common.js */ + +/** + * Tests if the given child and grand child accessibles at the given point are + * expected. + * + * @param aID [in] accessible identifier + * @param aX [in] x coordinate of the point relative accessible + * @param aY [in] y coordinate of the point relative accessible + * @param aChildID [in] expected child accessible + * @param aGrandChildID [in] expected child accessible + */ +function testChildAtPoint(aID, aX, aY, aChildID, aGrandChildID) { + var child = getChildAtPoint(aID, aX, aY, false); + var expectedChild = getAccessible(aChildID); + + var msg = + "Wrong direct child accessible at the point (" + + aX + + ", " + + aY + + ") of " + + prettyName(aID); + isObject(child, expectedChild, msg); + + var grandChild = getChildAtPoint(aID, aX, aY, true); + var expectedGrandChild = getAccessible(aGrandChildID); + + msg = + "Wrong deepest child accessible at the point (" + + aX + + ", " + + aY + + ") of " + + prettyName(aID); + isObject(grandChild, expectedGrandChild, msg); +} + +/** + * Test if getChildAtPoint returns the given child and grand child accessibles + * at coordinates of child accessible (direct and deep hit test). + */ +function hitTest(aContainerID, aChildID, aGrandChildID) { + var container = getAccessible(aContainerID); + var child = getAccessible(aChildID); + var grandChild = getAccessible(aGrandChildID); + + var [x, y] = getBoundsForDOMElm(child); + + var actualChild = container.getChildAtPoint(x + 1, y + 1); + isObject( + actualChild, + child, + "Wrong direct child of " + prettyName(aContainerID) + ); + + var actualGrandChild = container.getDeepestChildAtPoint(x + 1, y + 1); + isObject( + actualGrandChild, + grandChild, + "Wrong deepest child of " + prettyName(aContainerID) + ); +} + +/** + * Test if getOffsetAtPoint returns the given text offset at given coordinates. + */ +function testOffsetAtPoint(aHyperTextID, aX, aY, aCoordType, aExpectedOffset) { + var hyperText = getAccessible(aHyperTextID, [nsIAccessibleText]); + var offset = hyperText.getOffsetAtPoint(aX, aY, aCoordType); + is( + offset, + aExpectedOffset, + "Wrong offset at given point (" + + aX + + ", " + + aY + + ") for " + + prettyName(aHyperTextID) + ); +} + +/** + * Zoom the given document. + */ +function zoomDocument(aDocument, aZoom) { + SpecialPowers.setFullZoom(aDocument.defaultView, aZoom); +} + +/** + * Set the relative resolution of this document. This is what apz does. + * On non-mobile platforms you won't see a visible change. + */ +function setResolution(aDocument, aZoom) { + var windowUtils = aDocument.defaultView.windowUtils; + + windowUtils.setResolutionAndScaleTo(aZoom); +} + +/** + * Return child accessible at the given point. + * + * @param aIdentifier [in] accessible identifier + * @param aX [in] x coordinate of the point relative accessible + * @param aY [in] y coordinate of the point relative accessible + * @param aFindDeepestChild [in] points whether deepest or nearest child should + * be returned + * @return the child accessible at the given point + */ +function getChildAtPoint(aIdentifier, aX, aY, aFindDeepestChild) { + var acc = getAccessible(aIdentifier); + if (!acc) { + return null; + } + + var [screenX, screenY] = getBoundsForDOMElm(acc.DOMNode); + + var x = screenX + aX; + var y = screenY + aY; + + try { + if (aFindDeepestChild) { + return acc.getDeepestChildAtPoint(x, y); + } + return acc.getChildAtPoint(x, y); + } catch (e) {} + + return null; +} + +/** + * Test the accessible position. + */ +function testPos(aID, aPoint) { + var [expectedX, expectedY] = + aPoint != undefined ? aPoint : getBoundsForDOMElm(aID); + + var [x, y] = getBounds(aID); + is(x, expectedX, "Wrong x coordinate of " + prettyName(aID)); + is(y, expectedY, "Wrong y coordinate of " + prettyName(aID)); +} + +/** + * Test the accessible boundaries. + */ +function testBounds(aID, aRect) { + var [expectedX, expectedY, expectedWidth, expectedHeight] = + aRect != undefined ? aRect : getBoundsForDOMElm(aID); + + var [x, y, width, height] = getBounds(aID); + is(x, expectedX, "Wrong x coordinate of " + prettyName(aID)); + is(y, expectedY, "Wrong y coordinate of " + prettyName(aID)); + is(width, expectedWidth, "Wrong width of " + prettyName(aID)); + is(height, expectedHeight, "Wrong height of " + prettyName(aID)); +} + +/** + * Test text position at the given offset. + */ +function testTextPos(aID, aOffset, aPoint, aCoordOrigin) { + var [expectedX, expectedY] = aPoint; + + var xObj = {}, + yObj = {}; + var hyperText = getAccessible(aID, [nsIAccessibleText]); + hyperText.getCharacterExtents(aOffset, xObj, yObj, {}, {}, aCoordOrigin); + is( + xObj.value, + expectedX, + "Wrong x coordinate at offset " + aOffset + " for " + prettyName(aID) + ); + ok( + yObj.value - expectedY < 2 && expectedY - yObj.value < 2, + "Wrong y coordinate at offset " + + aOffset + + " for " + + prettyName(aID) + + " - got " + + yObj.value + + ", expected " + + expectedY + + "The difference doesn't exceed 1." + ); +} + +/** + * Test text bounds that is enclosed betwene the given offsets. + */ +function testTextBounds(aID, aStartOffset, aEndOffset, aRect, aCoordOrigin) { + var [expectedX, expectedY, expectedWidth, expectedHeight] = aRect; + + var xObj = {}, + yObj = {}, + widthObj = {}, + heightObj = {}; + var hyperText = getAccessible(aID, [nsIAccessibleText]); + hyperText.getRangeExtents( + aStartOffset, + aEndOffset, + xObj, + yObj, + widthObj, + heightObj, + aCoordOrigin + ); + + // x + is( + xObj.value, + expectedX, + "Wrong x coordinate of text between offsets (" + + aStartOffset + + ", " + + aEndOffset + + ") for " + + prettyName(aID) + ); + + // y + isWithin( + yObj.value, + expectedY, + 1, + `y coord of text between offsets (${aStartOffset}, ${aEndOffset}) ` + + `for ${prettyName(aID)}` + ); + + // Width + var msg = + "Wrong width of text between offsets (" + + aStartOffset + + ", " + + aEndOffset + + ") for " + + prettyName(aID); + if (widthObj.value == expectedWidth) { + ok(true, msg); + } else { + todo(false, msg); + } // fails on some windows machines + + // Height + isWithin( + heightObj.value, + expectedHeight, + 1, + `height of text between offsets (${aStartOffset}, ${aEndOffset}) ` + + `for ${prettyName(aID)}` + ); +} + +/** + * Return the accessible coordinates relative to the screen in device pixels. + */ +function getPos(aID) { + var accessible = getAccessible(aID); + var x = {}, + 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. + */ +function getBounds(aID, aDPR = window.devicePixelRatio) { + const accessible = getAccessible(aID); + let x = {}, + y = {}, + width = {}, + height = {}; + let xInCSS = {}, + yInCSS = {}, + widthInCSS = {}, + heightInCSS = {}; + accessible.getBounds(x, y, width, height); + accessible.getBoundsInCSSPixels(xInCSS, yInCSS, widthInCSS, heightInCSS); + + isWithin( + x.value / aDPR, + xInCSS.value, + 1, + "Heights in CSS pixels is calculated correctly" + ); + isWithin( + y.value / aDPR, + yInCSS.value, + 1, + "Heights in CSS pixels is calculated correctly" + ); + isWithin( + width.value / aDPR, + widthInCSS.value, + 1, + "Heights in CSS pixels is calculated correctly" + ); + isWithin( + height.value / aDPR, + heightInCSS.value, + 1, + "Heights in CSS pixels is calculated correctly" + ); + + return [x.value, y.value, width.value, height.value]; +} + +function getRangeExtents(aID, aStartOffset, aEndOffset, aCoordOrigin) { + var hyperText = getAccessible(aID, [nsIAccessibleText]); + var x = {}, + y = {}, + width = {}, + height = {}; + hyperText.getRangeExtents( + aStartOffset, + aEndOffset, + x, + y, + width, + height, + aCoordOrigin + ); + return [x.value, y.value, width.value, height.value]; +} + +/** + * Return DOM node coordinates relative the screen and its size in device + * pixels. + */ +function getBoundsForDOMElm(aID) { + var x = 0, + y = 0, + width = 0, + height = 0; + + var elm = getNode(aID); + if (elm.localName == "area") { + var mapName = elm.parentNode.getAttribute("name"); + var selector = "[usemap='#" + mapName + "']"; + var img = elm.ownerDocument.querySelector(selector); + + var areaCoords = elm.coords.split(","); + var areaX = parseInt(areaCoords[0]); + var areaY = parseInt(areaCoords[1]); + var areaWidth = parseInt(areaCoords[2]) - areaX; + var areaHeight = parseInt(areaCoords[3]) - areaY; + + let rect = img.getBoundingClientRect(); + x = rect.left + areaX; + y = rect.top + areaY; + width = areaWidth; + height = areaHeight; + } else { + let rect = elm.getBoundingClientRect(); + x = rect.left; + y = rect.top; + width = rect.width; + height = rect.height; + } + + var elmWindow = elm.ownerGlobal; + return CSSToDevicePixels( + elmWindow, + x + elmWindow.mozInnerScreenX, + y + elmWindow.mozInnerScreenY, + width, + height + ); +} + +function CSSToDevicePixels(aWindow, aX, aY, aWidth, aHeight) { + var winUtil = aWindow.windowUtils; + + var 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(aX * ratio), + Math.round(aY * ratio), + Math.round(aWidth * ratio), + Math.round(aHeight * ratio), + ]; +} diff --git a/accessible/tests/mochitest/letters.gif b/accessible/tests/mochitest/letters.gif Binary files differnew file mode 100644 index 0000000000..299b91784a --- /dev/null +++ b/accessible/tests/mochitest/letters.gif diff --git a/accessible/tests/mochitest/longdesc_src.html b/accessible/tests/mochitest/longdesc_src.html new file mode 100644 index 0000000000..37248795dd --- /dev/null +++ b/accessible/tests/mochitest/longdesc_src.html @@ -0,0 +1,8 @@ +<html> +<head> +<title>Mozilla logo</title> +</head> +<body> +<p>This file would contain a longer description of the Mozilla logo, if I knew what it looked like.</p> +</body> +</html> diff --git a/accessible/tests/mochitest/moz.build b/accessible/tests/mochitest/moz.build new file mode 100644 index 0000000000..d3efffb62b --- /dev/null +++ b/accessible/tests/mochitest/moz.build @@ -0,0 +1,37 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# 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/. + +A11Y_MANIFESTS += [ + "a11y.ini", + "actions/a11y.ini", + "aom/a11y.ini", + "attributes/a11y.ini", + "bounds/a11y.ini", + "editabletext/a11y.ini", + "elm/a11y.ini", + "events/a11y.ini", + "events/docload/a11y.ini", + "focus/a11y.ini", + "hittest/a11y.ini", + "hyperlink/a11y.ini", + "hypertext/a11y.ini", + "name/a11y.ini", + "pivot/a11y.ini", + "relations/a11y.ini", + "role/a11y.ini", + "scroll/a11y.ini", + "selectable/a11y.ini", + "states/a11y.ini", + "table/a11y.ini", + "text/a11y.ini", + "textattrs/a11y.ini", + "textcaret/a11y.ini", + "textrange/a11y.ini", + "textselection/a11y.ini", + "tree/a11y.ini", + "treeupdate/a11y.ini", + "value/a11y.ini", +] diff --git a/accessible/tests/mochitest/moz.png b/accessible/tests/mochitest/moz.png Binary files differnew file mode 100644 index 0000000000..743292dc6f --- /dev/null +++ b/accessible/tests/mochitest/moz.png diff --git a/accessible/tests/mochitest/name.js b/accessible/tests/mochitest/name.js new file mode 100644 index 0000000000..48bf2d7038 --- /dev/null +++ b/accessible/tests/mochitest/name.js @@ -0,0 +1,38 @@ +/* import-globals-from common.js */ + +/** + * Test accessible name for the given accessible identifier. + */ +function testName(aAccOrElmOrID, aName, aMsg, aTodo) { + var msg = aMsg ? aMsg : ""; + + var acc = getAccessible(aAccOrElmOrID); + if (!acc) { + return ""; + } + + var func = aTodo ? todo_is : is; + var txtID = prettyName(aAccOrElmOrID); + try { + func(acc.name, aName, msg + "Wrong name of the accessible for " + txtID); + } catch (e) { + ok(false, msg + "Can't get name of the accessible for " + txtID); + } + return acc; +} + +/** + * Test accessible description for the given accessible. + */ +function testDescr(aAccOrElmOrID, aDescr) { + var acc = getAccessible(aAccOrElmOrID); + if (!acc) { + return; + } + + is( + acc.description, + aDescr, + "Wrong description for " + prettyName(aAccOrElmOrID) + ); +} diff --git a/accessible/tests/mochitest/name/a11y.ini b/accessible/tests/mochitest/name/a11y.ini new file mode 100644 index 0000000000..cfcb49d816 --- /dev/null +++ b/accessible/tests/mochitest/name/a11y.ini @@ -0,0 +1,18 @@ +[DEFAULT] +support-files = + markup.js + markuprules.xml + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/moz.png + +[test_ARIACore_examples.html] +[test_browserui.xhtml] +[test_counterstyle.html] +[test_general.html] +[test_general.xhtml] +[test_link.html] +[test_list.html] +[test_markup.html] +skip-if = (debug && os == 'win') # Bug 1296784 +[test_svg.html] +[test_tree.xhtml] diff --git a/accessible/tests/mochitest/name/markup.js b/accessible/tests/mochitest/name/markup.js new file mode 100644 index 0000000000..261a3790cd --- /dev/null +++ b/accessible/tests/mochitest/name/markup.js @@ -0,0 +1,438 @@ +/* import-globals-from ../attributes.js */ +/* import-globals-from ../common.js */ +/* import-globals-from ../events.js */ +/* import-globals-from ../name.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Name tests described by "markuprules.xml" file. + +var gNameRulesFileURL = "markuprules.xml"; + +var gRuleDoc = null; + +// Debuggin stuff. +var gDumpToConsole = false; + +/** + * Start name tests. Run through markup elements and test names for test + * element (see namerules.xml for details). + */ +function testNames() { + // enableLogging("tree,stack"); // debugging + + var request = new XMLHttpRequest(); + request.open("get", gNameRulesFileURL, false); + request.send(); + + gRuleDoc = request.responseXML; + + var markupElms = evaluateXPath(gRuleDoc, "//rules/rulesample/markup"); + gTestIterator.iterateMarkups(markupElms); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private section. + +/** + * Helper class to interate through name tests. + */ +var gTestIterator = { + iterateMarkups: function gTestIterator_iterateMarkups(aMarkupElms) { + this.markupElms = aMarkupElms; + + this.iterateNext(); + }, + + iterateRules: function gTestIterator_iterateRules( + aElm, + aContainer, + aRuleSetElm, + aRuleElms, + aTestID + ) { + this.ruleSetElm = aRuleSetElm; + this.ruleElms = aRuleElms; + this.elm = aElm; + this.container = aContainer; + this.testID = aTestID; + + this.iterateNext(); + }, + + iterateNext: function gTestIterator_iterateNext() { + if (this.markupIdx == -1) { + this.markupIdx++; + testNamesForMarkup(this.markupElms[this.markupIdx]); + return; + } + + this.ruleIdx++; + if (this.ruleIdx == this.ruleElms.length) { + // When test is finished then name is empty and no explict-name. + var defaultName = this.ruleSetElm.hasAttribute("defaultName") + ? this.ruleSetElm.getAttribute("defaultName") + : null; + testName( + this.elm, + defaultName, + "Default name test (" + gTestIterator.testID + "). " + ); + testAbsentAttrs(this.elm, { "explicit-name": "true" }); + + this.markupIdx++; + if (this.markupIdx == this.markupElms.length) { + // disableLogging("tree"); // debugging + SimpleTest.finish(); + return; + } + + this.ruleIdx = -1; + + if (gDumpToConsole) { + dump( + "\nPend next markup processing. Wait for reorder event on " + + prettyName(document) + + "'\n" + ); + } + waitForEvent( + EVENT_REORDER, + document, + testNamesForMarkup, + null, + this.markupElms[this.markupIdx] + ); + + document.body.removeChild(this.container); + return; + } + + testNameForRule(this.elm, this.ruleElms[this.ruleIdx]); + }, + + markupElms: null, + markupIdx: -1, + rulesetElm: null, + ruleElms: null, + ruleIdx: -1, + elm: null, + container: null, + testID: "", +}; + +/** + * Process every 'markup' element and test names for it. Used by testNames + * function. + */ +function testNamesForMarkup(aMarkupElm) { + if (gDumpToConsole) { + dump("\nProcessing markup '" + aMarkupElm.getAttribute("id") + "'\n"); + } + + var div = document.createElement("div"); + div.setAttribute("id", "test"); + + var child = aMarkupElm.firstChild; + while (child) { + var newChild = document.importNode(child, true); + div.appendChild(newChild); + child = child.nextSibling; + } + + if (gDumpToConsole) { + dump( + "\nProcessing markup. Wait for reorder event on " + + prettyName(document) + + "'\n" + ); + } + waitForEvent( + EVENT_REORDER, + document, + testNamesForMarkupRules, + null, + aMarkupElm, + div + ); + + document.body.appendChild(div); +} + +function testNamesForMarkupRules(aMarkupElm, aContainer) { + var testID = aMarkupElm.getAttribute("id"); + if (gDumpToConsole) { + dump("\nProcessing markup rules '" + testID + "'\n"); + } + + var expr = "//html/body/div[@id='test']/" + aMarkupElm.getAttribute("ref"); + var elm = evaluateXPath(document, expr, htmlDocResolver)[0]; + + var ruleId = aMarkupElm.getAttribute("ruleset"); + var ruleElm = gRuleDoc.querySelector("[id='" + ruleId + "']"); + var ruleElms = getRuleElmsByRulesetId(ruleId); + + var processMarkupRules = gTestIterator.iterateRules.bind( + gTestIterator, + elm, + aContainer, + ruleElm, + ruleElms, + testID + ); + + // Images may be recreated after we append them into subtree. We need to wait + // in this case. If we are on profiling enabled build then stack tracing + // works and thus let's log instead. Note, that works if you enabled logging + // (refer to testNames() function). + if (isAccessible(elm) || isLogged("stack")) { + processMarkupRules(); + } else { + waitForEvent(EVENT_SHOW, elm, processMarkupRules); + } +} + +/** + * Test name for current rule and current 'markup' element. Used by + * testNamesForMarkup function. + */ +function testNameForRule(aElm, aRuleElm) { + if (aRuleElm.hasAttribute("attr")) { + if (gDumpToConsole) { + dump( + "\nProcessing rule { attr: " + aRuleElm.getAttribute("attr") + " }\n" + ); + } + + testNameForAttrRule(aElm, aRuleElm); + } else if (aRuleElm.hasAttribute("elm")) { + if (gDumpToConsole) { + dump( + "\nProcessing rule { elm: " + + aRuleElm.getAttribute("elm") + + ", elmattr: " + + aRuleElm.getAttribute("elmattr") + + " }\n" + ); + } + + testNameForElmRule(aElm, aRuleElm); + } else if (aRuleElm.getAttribute("fromsubtree") == "true") { + if (gDumpToConsole) { + dump( + "\nProcessing rule { fromsubtree: " + + aRuleElm.getAttribute("fromsubtree") + + " }\n" + ); + } + + testNameForSubtreeRule(aElm, aRuleElm); + } +} + +function testNameForAttrRule(aElm, aRule) { + var name = ""; + + var attr = aRule.getAttribute("attr"); + var attrValue = aElm.getAttribute(attr); + + var type = aRule.getAttribute("type"); + if (type == "string") { + name = attrValue; + } else if (type == "ref" && attrValue) { + var ids = attrValue.split(/\s+/); + for (var idx = 0; idx < ids.length; idx++) { + var labelElm = getNode(ids[idx]); + if (name != "") { + name += " "; + } + + name += labelElm.getAttribute("textequiv"); + } + } + + var msg = "Attribute '" + attr + "' test (" + gTestIterator.testID + "). "; + testName(aElm, name, msg); + + if (aRule.getAttribute("explict-name") != "false") { + testAttrs(aElm, { "explicit-name": "true" }, true); + } else { + testAbsentAttrs(aElm, { "explicit-name": "true" }); + } + + // If @recreated attribute is used then this attribute change recreates an + // accessible. Wait for reorder event in this case or otherwise proceed next + // test immediately. + if (aRule.hasAttribute("recreated")) { + waitForEvent( + EVENT_REORDER, + aElm.parentNode, + gTestIterator.iterateNext, + gTestIterator + ); + aElm.removeAttribute(attr); + } else if (aRule.hasAttribute("textchanged")) { + waitForEvent( + EVENT_TEXT_INSERTED, + aElm, + gTestIterator.iterateNext, + gTestIterator + ); + aElm.removeAttribute(attr); + } else if (aRule.hasAttribute("contentchanged")) { + waitForEvent(EVENT_REORDER, aElm, gTestIterator.iterateNext, gTestIterator); + aElm.removeAttribute(attr); + } else { + aElm.removeAttribute(attr); + gTestIterator.iterateNext(); + } +} + +function testNameForElmRule(aElm, aRule) { + var labelElm; + + var tagname = aRule.getAttribute("elm"); + var attrname = aRule.getAttribute("elmattr"); + if (attrname) { + var filter = { + acceptNode: function filter_acceptNode(aNode) { + if ( + aNode.localName == this.mLocalName && + aNode.getAttribute(this.mAttrName) == this.mAttrValue + ) { + return NodeFilter.FILTER_ACCEPT; + } + + return NodeFilter.FILTER_SKIP; + }, + + mLocalName: tagname, + mAttrName: attrname, + mAttrValue: aElm.getAttribute("id"), + }; + + var treeWalker = document.createTreeWalker( + document.body, + NodeFilter.SHOW_ELEMENT, + filter + ); + labelElm = treeWalker.nextNode(); + } else { + // if attrname is empty then look for the element in subtree. + labelElm = aElm.getElementsByTagName(tagname)[0]; + if (!labelElm) { + labelElm = aElm.getElementsByTagName("html:" + tagname)[0]; + } + } + + if (!labelElm) { + ok(false, msg + " Failed to find '" + tagname + "' element."); + gTestIterator.iterateNext(); + return; + } + + var msg = "Element '" + tagname + "' test (" + gTestIterator.testID + ")."; + testName(aElm, labelElm.getAttribute("textequiv"), msg); + testAttrs(aElm, { "explicit-name": "true" }, true); + + var parentNode = labelElm.parentNode; + + if (gDumpToConsole) { + dump( + "\nProcessed elm rule. Wait for reorder event on " + + prettyName(parentNode) + + "\n" + ); + } + waitForEvent( + EVENT_REORDER, + parentNode, + gTestIterator.iterateNext, + gTestIterator + ); + + parentNode.removeChild(labelElm); +} + +function testNameForSubtreeRule(aElm, aRule) { + var msg = "From subtree test (" + gTestIterator.testID + ")."; + testName(aElm, aElm.getAttribute("textequiv"), msg); + testAbsentAttrs(aElm, { "explicit-name": "true" }); + + if (gDumpToConsole) { + dump( + "\nProcessed from subtree rule. Wait for reorder event on " + + prettyName(aElm) + + "\n" + ); + } + waitForEvent(EVENT_REORDER, aElm, gTestIterator.iterateNext, gTestIterator); + + while (aElm.firstChild) { + aElm.firstChild.remove(); + } +} + +/** + * Return array of 'rule' elements. Used in conjunction with + * getRuleElmsFromRulesetElm() function. + */ +function getRuleElmsByRulesetId(aRulesetId) { + var expr = "//rules/ruledfn/ruleset[@id='" + aRulesetId + "']"; + var rulesetElm = evaluateXPath(gRuleDoc, expr); + return getRuleElmsFromRulesetElm(rulesetElm[0]); +} + +function getRuleElmsFromRulesetElm(aRulesetElm) { + var rulesetId = aRulesetElm.getAttribute("ref"); + if (rulesetId) { + return getRuleElmsByRulesetId(rulesetId); + } + + var ruleElms = []; + + var child = aRulesetElm.firstChild; + while (child) { + if (child.localName == "ruleset") { + ruleElms = ruleElms.concat(getRuleElmsFromRulesetElm(child)); + } + if (child.localName == "rule") { + ruleElms.push(child); + } + + child = child.nextSibling; + } + + return ruleElms; +} + +/** + * Helper method to evaluate xpath expression. + */ +function evaluateXPath(aNode, aExpr, aResolver) { + var xpe = new XPathEvaluator(); + + var resolver = aResolver; + if (!resolver) { + var node = + aNode.ownerDocument == null + ? aNode.documentElement + : aNode.ownerDocument.documentElement; + resolver = xpe.createNSResolver(node); + } + + var result = xpe.evaluate(aExpr, aNode, resolver, 0, null); + var found = []; + var res; + while ((res = result.iterateNext())) { + found.push(res); + } + + return found; +} + +function htmlDocResolver(aPrefix) { + var ns = { + html: "http://www.w3.org/1999/xhtml", + }; + return ns[aPrefix] || null; +} diff --git a/accessible/tests/mochitest/name/markuprules.xml b/accessible/tests/mochitest/name/markuprules.xml new file mode 100644 index 0000000000..7da43bb33e --- /dev/null +++ b/accessible/tests/mochitest/name/markuprules.xml @@ -0,0 +1,385 @@ +<?xml version="1.0"?> + +<!-- + This XML file is used to create sequence of accessible name tests. It consist + of two sections. The first section 'ruledfn' declares name computation rules. + The second section 'rulesample' defines markup samples we need to check name + computation rules for. + + <ruledfn> + <ruleset> + <rule> + + Section 'ruledfn' contains 'ruleset' elements. Every 'ruleset' element is + presented by 'rule' elements so that sequence of 'rule' elements gives the + sequence of name computations rules. Every 'rule' element can be one of four + types. + + * <rule attr='' type='string'/> used when name is equal to the value of + attribute presented on the element. + + Example, 'aria-label' attribute. In this case 'rule' element has 'attr' + attribute pointing to attribute name and 'type' attribute with 'string' + value. For example, <rule attr="aria-label" type="string"/>. + + * <rule attr='' type='ref'/> used when name is calculated from elements that + are pointed to by attribute value on the element. + + Example is 'aria-labelledby'. In this case 'rule' element has 'attr' + attribute holding the sequence of IDs of elements used to compute the name, + in addition the 'rule' element has 'type' attribute with 'ref' value. + For example, <rule attr="aria-labelledby" type="ref"/>. + + * <rule elm='' elmattr=''/> used when name is calculated from another + element. These attributes are used to find an element by tagname and + attribute with value equaled to ID of the element. If 'elmattr' is missed + then element from subtree with the given tagname is used. + + Example, html:label@for element, <rule elm="label" elmattr="for"/>. + Example, html:caption element, <rule elm="caption"/> + + * <rule fromsubtree='true'/> used when name is computed from subtree. + + Example, html:button. In this case 'rule' element has 'fromsubtree' + attribute with 'true' value. + + <rulesample> + <markup ruleset=''> + + Section 'rulesample' provides set of markup samples ('markup' elements). Every + 'markup' element contains an element that accessible name will be computed for + (let's call it test element). In addition the 'markup' element contains some + other elements from native markup used in name calculation process for test + element. Test element is pointed to by 'ref' attribute on 'markup' element. + Also 'markup' element has 'ruleset' attribute to indicate ruleset for the test + element. + + How does it work? Let's consider simple example: + <ruledfn> + <ruleset id="aria"> + <rule attr="aria-label" type="string"/> + <rule attr="aria-labelledby" type="ref"/> + </ruleset> + </ruledfn> + <rulesample> + <markup ref="html:div" ruleset="aria"> + <html:span id="label" textequiv="test2">test2</html:span> + <html:div aria-label="test1" + aria-labelledby="label">it's a div</html:div> + </markup> + </rulesample> + + Initially 'markup' element holds markup for all rules specified by 'ruleset' + attribute. This allows us to check if the sequence of name computation rules + is correct. Here 'ruleset' element defines two rules. We get the first rule + which means accessible name is computed from value of 'aria-label' attribute. + Then we check accessible name for the test element and remove 'aria-label' + attribute. After we get the second rule which means we should get IDs from + 'aria-labelledby' attribute and compose accessible name from values of + 'textequiv' attributes (that are supposed to give the desired name for each + element that is being pointed to by aria-labelledby). Check accessible name + and finish test. +--> + +<rules xmlns:html="http://www.w3.org/1999/xhtml" + xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"> + + <ruledfn> + + <!-- bricks --> + <ruleset id="ARIA"> + <rule attr="aria-labelledby" type="ref"/> + <rule attr="aria-label" type="string"/> + </ruleset> + + <ruleset id="HTMLControl:Head"> + <ruleset ref="ARIA"/> + <rule elm="label" elmattr="for"/> + </ruleset> + + <!-- general --> + <ruleset id="HTMLControl"> + <ruleset ref="HTMLControl:Head"/> + <rule fromsubtree="true"/> + <rule attr="title" type="string"/> + </ruleset> + + <ruleset id="HTMLElm"> + <ruleset ref="ARIA"/> + <rule attr="title" type="string"/> + </ruleset> + + <!-- specific --> + <ruleset id="HTMLARIAGridCell"> + <ruleset ref="ARIA"/> + <rule fromsubtree="true"/> + <rule attr="title" type="string"/> + </ruleset> + + <ruleset id="HTMLInputButton"> + <ruleset ref="HTMLControl:Head"/> + <rule attr="value" type="string" explict-name="false" reordered="true"/> + <rule attr="title" type="string"/> + </ruleset> + + <ruleset id="HTMLInputSubmit" defaultName="Submit Query"> + <ruleset ref="HTMLControl:Head"/> + <rule attr="value" type="string" explict-name="false" textchanged="true"/> + </ruleset> + + <ruleset id="HTMLInputReset" defaultName="Reset"> + <ruleset ref="HTMLControl:Head"/> + <rule attr="value" type="string" explict-name="false" textchanged="true"/> + </ruleset> + + <ruleset id="HTMLInputImage"> + <ruleset ref="HTMLControl:Head"/> + <rule attr="alt" type="string" textchanged="true"/> + <rule attr="value" type="string" textchanged="true"/> + <rule attr="title" type="string"/> + </ruleset> + + <ruleset id="HTMLInputImageNoValidSrc" defaultName="Submit Query"> + <ruleset ref="HTMLControl:Head"/> + <rule attr="alt" type="string" explict-name="false" textchanged="true"/> + <rule attr="value" type="string" explict-name="false" textchanged="true"/> + </ruleset> + + <ruleset id="HTMLOption"> + <ruleset ref="ARIA"/> + <rule attr="label" type="string"/> + <rule fromsubtree="true"/> + <rule attr="title" type="string"/> + </ruleset> + + <ruleset id="HTMLImg"> + <ruleset ref="ARIA"/> + <rule attr="alt" type="string" recreated="true"/> + <rule attr="title" type="string"/> + </ruleset> + + <ruleset id="HTMLImgEmptyAlt"> + <ruleset ref="ARIA"/> + <rule attr="title" type="string"/> + <rule attr="alt" type="string" recreated="true"/> + </ruleset> + + <ruleset id="HTMLTable"> + <ruleset ref="ARIA"/> + <rule elm="caption"/> + <rule attr="summary" type="string"/> + <rule attr="title" type="string"/> + </ruleset> + </ruledfn> + + <rulesample> + + <markup id="HTMLButtonTest" + ref="html:button" ruleset="HTMLControl"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:label for="btn" textequiv="test4">test4</html:label> + <html:button id="btn" + aria-label="test1" + aria-labelledby="l1 l2" + title="test5" + textequiv="press me">press me</html:button> + </markup> + + <markup id="HTMLInputButtonTest" + ref="html:input" ruleset="HTMLInputButton"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:label for="btn" textequiv="test4">test4</html:label> + <html: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"/> + </markup> + + <markup id="HTMLInputSubmitTest" + ref="html:input" ruleset="HTMLInputSubmit"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:label for="btn-submit" textequiv="test4">test4</html:label> + <html: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"/> + </markup> + + <markup id="HTMLInputResetTest" + ref="html:input" ruleset="HTMLInputReset"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:label for="btn-reset" textequiv="test4">test4</html:label> + <html: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"/> + </markup> + + <!-- + Disabled due to intermittent failures (bug 1436323) which became more + frequent due to the landing of bug 1383682. The latter bug made loading + of images from cache much more consistent, which appears to have impacted + the timing for this test case. If the image is switched to a unique + image (e.g. always decoding since there is no cache), the failure rate + increases, presumably because the test is dependent on a specific ordering + of events, and implicitly assumes the image is loaded immediately. + --> + + <!-- + <markup id="HTMLInputImageTest" + ref="html:input" ruleset="HTMLInputImage"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:label for="btn-image" textequiv="test4">test4</html:label> + <html:input id="btn-image" + type="image" + aria-label="test1" + aria-labelledby="l1 l2" + alt="name from alt" + value="name from value" + src="../moz.png" + data="no name from data" + title="name from title"/> + </markup> + --> + + <markup id="HTMLInputImageNoValidSrcTest" + ref="html:input" ruleset="HTMLInputImageNoValidSrc"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:label for="btn-image" textequiv="test4">test4</html:label> + <html: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"/> + </markup> + + <markup id="HTMLOptionTest" + ref="html:select/html:option[1]" ruleset="HTMLOption"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:select> + <html:option id="opt" + aria-label="test1" + aria-labelledby="l1 l2" + label="test4" + title="test5" + textequiv="option1">option1</html:option> + <html:option>option2</html:option> + </html:select> + </markup> + + <markup id="HTMLImageTest" + ref="html:img" ruleset="HTMLImg"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:img id="img" + aria-label="Logo of Mozilla" + aria-labelledby="l1 l2" + alt="Mozilla logo" + title="This is a logo" + src="../moz.png"/> + </markup> + + <markup id="HTMLImageEmptyAltTest" + ref="html:img" ruleset="HTMLImgEmptyAlt"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:img id="imgemptyalt" + aria-label="Logo of Mozilla" + aria-labelledby="l1 l2" + title="This is a logo" + alt="" + src="../moz.png"/> + </markup> + + <markup id="HTMLTdTest" + ref="html:table/html:tr/html:td" ruleset="HTMLElm"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:label for="tc" textequiv="test4">test4</html:label> + <html:table> + <html:tr> + <html:td id="tc" + aria-label="test1" + aria-labelledby="l1 l2" + title="test5"> + <html:p>This is a paragraph</html:p> + <html:a href="#">This is a link</html:a> + <html:ul> + <html:li>This is a list</html:li> + </html:ul> + </html:td> + </html:tr> + </html:table> + </markup> + + <markup id="HTMLTdARIAGridCellTest" + ref="html:table/html:tr/html:td" ruleset="HTMLARIAGridCell"> + <html:span id="l1" textequiv="test2">test2</html:span> + <html:span id="l2" textequiv="test3">test3</html:span> + <html:label for="gc" textequiv="test4">test4</html:label> + <html:table> + <html:tr> + <html:td id="gc" + role="gridcell" + aria-label="test1" + aria-labelledby="l1 l2" + textequiv="This is a paragraph This is a link • Listitem1 • Listitem2" + title="This is a paragraph This is a link This is a list"> + <html:p>This is a paragraph</html:p> + <html:a href="#">This is a link</html:a> + <html:ul> + <html:li>Listitem1</html:li> + <html:li>Listitem2</html:li> + </html:ul> + </html:td> + </html:tr> + </html:table> + </markup> + + <markup id="HTMLTableTest" + ref="html:table" ruleset="HTMLTable"> + <html:span id="l1" textequiv="lby_tst6_1">lby_tst6_1</html:span> + <html:span id="l2" textequiv="lby_tst6_2">lby_tst6_2</html:span> + <html:label for="t" textequiv="label_tst6">label_tst6</html:label> + <!-- layout frame are recreated due to varous reasons, here's text frame + placed after caption frame triggres table frame recreation when + caption element is removed from DOM; get rid text node after caption + node to make the test working --> + <html:table id="t" aria-label="arialabel_tst6" + aria-labelledby="l1 l2" + summary="summary_tst6" + title="title_tst6"> + <html:caption textequiv="caption_tst6">caption_tst6</html:caption><html:tr> + <html:td>cell1</html:td> + <html:td>cell2</html:td> + </html:tr> + </html:table> + </markup> + + </rulesample> +</rules> diff --git a/accessible/tests/mochitest/name/test_ARIACore_examples.html b/accessible/tests/mochitest/name/test_ARIACore_examples.html new file mode 100644 index 0000000000..a15fee78f8 --- /dev/null +++ b/accessible/tests/mochitest/name/test_ARIACore_examples.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Testing a11y name ccomputation testcases</title> + + <link rel="stylesheet" + type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <script type="application/javascript"> + function doTest() { + // All test cases taken from https://www.w3.org/TR/accname-1.1/ + // These were especially called out to demonstrate edge cases. + + // Example 1 from section 4.3.1 under 2.B. + // Element1 should get its name from the text in element3. + // Element2 should not gets name from element1 because that already + // gets its name from another element. + testName("el1", "hello"); + testName("el2", null); + + // Example 2 from section 4.3.1 under 2.C. + // The buttons should get their name from their labels and the links. + testName("del_row1", "Delete Documentation.pdf"); + testName("del_row2", "Delete HolidayLetter.pdf"); + + // Example 3 from section 4.3.1 under 2.F. + // Name should be own content text plus the value of the input plus + // more own inner text, separated by 1 space. + testName("chkbx", "Flash the screen 5 times"); + + // Example 4 from section 4.3.1 under 2.F. + // Name from content should include all the child nodes, including + // table cells. + testName("input_with_html_label", "foo bar baz"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- el1 should be labeled, el2 should not. --> + <div id="el1" aria-labelledby="el3"></div> + <div id="el2" aria-labelledby="el1"></div> + <div id="el3"> hello </div> + + <!-- The buttons should be labeled by themselves and the referenced link --> + <ul> + <li> + <a id="file_row1" href="./files/Documentation.pdf">Documentation.pdf</a> + <span role="button" tabindex="0" id="del_row1" aria-label="Delete" + aria-labelledby="del_row1 file_row1"></span> + </li> + <li> + <a id="file_row2" href="./files/HolidayLetter.pdf">HolidayLetter.pdf</a> + <span role="button" tabindex="0" id="del_row2" aria-label="Delete" + aria-labelledby="del_row2 file_row2"></span> + </li> + </ul> + + <!-- Label from combined text and subtree --> + <div id="chkbx" role="checkbox" aria-checked="false">Flash the screen + <span role="textbox" aria-multiline="false"> 5 </span> times</div> + + <!-- Label with name from content should include table --> + <input id="input_with_html_label" /> + <label for="input_with_html_label" id="label"> + <div>foo</div> + <table><tr><td>bar</td></tr></table> + <div>baz</div> + </label> + +</body> +</html> diff --git a/accessible/tests/mochitest/name/test_browserui.xhtml b/accessible/tests/mochitest/name/test_browserui.xhtml new file mode 100644 index 0000000000..b330b177c9 --- /dev/null +++ b/accessible/tests/mochitest/name/test_browserui.xhtml @@ -0,0 +1,85 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessibility Name Calculating Test."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + <![CDATA[ + const { BrowserTestUtils } = ChromeUtils.import( + "resource://testing-common/BrowserTestUtils.jsm"); + const ABOUT_MOZILLA_URL = "about:mozilla"; + const ABOUT_LICENSE_URL = "about:license"; + + SimpleTest.waitForExplicitFinish(); + + (async () => { + info("Opening a new browser window."); + const win = await BrowserTestUtils.openNewBrowserWindow({ + remote: false, + fission: false, + }); + const winFocused = SimpleTest.promiseFocus(win); + const loaded = BrowserTestUtils.browserLoaded( + win.gBrowser.selectedBrowser); + let docLoaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event => + event.accessible.QueryInterface(nsIAccessibleDocument).URL === ABOUT_LICENSE_URL, + `Loaded tab: ${ABOUT_LICENSE_URL}`); + BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, + "about:license"); + await loaded; + await docLoaded; + await winFocused; + + info(`Loading a new tab: ${ABOUT_MOZILLA_URL}.`); + docLoaded = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, event => + event.accessible.QueryInterface(nsIAccessibleDocument).URL === ABOUT_MOZILLA_URL, + `Added tab: ${ABOUT_MOZILLA_URL}`); + const tab = win.gBrowser.addTrustedTab(ABOUT_MOZILLA_URL); + await BrowserTestUtils.browserLoaded(tab.linkedBrowser); + await docLoaded; + + info("Focusing on the newly opened tab."); + const focused = waitForEvent(EVENT_FOCUS, event => + event.DOMNode === win.gBrowser.getBrowserAtIndex(1).contentDocument); + await BrowserTestUtils.synthesizeKey("VK_TAB", { ctrlKey: true }, + win.gBrowser.selectedBrowser); + const focusEvent = await focused; + + const title = getAccessible(win.document).name; + const accName = focusEvent.accessible.name; + isnot(title.indexOf(accName), -1, + `Window title contains the name of active tab document (Is "${accName}" in "${title}"?)`); + + await BrowserTestUtils.closeWindow(win); + SimpleTest.finish(); + })(); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=507382" + title="focus is fired earlier than root accessible name is changed when switching between tabs"> + Mozilla Bug + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="eventdump"></vbox> + </vbox> +</window> diff --git a/accessible/tests/mochitest/name/test_counterstyle.html b/accessible/tests/mochitest/name/test_counterstyle.html new file mode 100644 index 0000000000..a20aca23f1 --- /dev/null +++ b/accessible/tests/mochitest/name/test_counterstyle.html @@ -0,0 +1,150 @@ +<html> + +<head> + <title>nsIAccessible::name calculation for @counter-style</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <style id="counterstyles" type="text/css"> + @counter-style system-alphabetic { + system: alphabetic; + symbols: x y z; + } + @counter-style system-cyclic { + system: cyclic; + symbols: x y z; + } + @counter-style system-numeric { + system: numeric; + symbols: x y z; + } + @counter-style speak-as-bullets { + system: extends decimal; + speak-as: bullets; + } + @counter-style speak-as-numbers { + system: extends system-alphabetic; + speak-as: numbers; + } + @counter-style speak-as-words { + system: additive; + additive-symbols: 20 "twenty ", 9 "nine", 7 "seven", 1 "one"; + speak-as: words; + } + @counter-style speak-as-spell-out { + system: extends system-alphabetic; + speak-as: spell-out; + } + @counter-style speak-as-other { + system: extends decimal; + speak-as: speak-as-words; + } + @counter-style speak-as-loop { + system: extends upper-latin; + speak-as: speak-as-loop0; + } + @counter-style speak-as-loop0 { + system: extends disc; + speak-as: speak-as-loop1; + } + @counter-style speak-as-loop1 { + system: extends decimal; + speak-as: speak-as-loop0; + } + @counter-style speak-as-extended0 { + system: extends decimal; + speak-as: speak-as-extended1; + } + @counter-style speak-as-extended1 { + system: extends speak-as-extended0; + speak-as: disc; + } + @counter-style speak-as-extended2 { + system: extends decimal; + speak-as: speak-as-extended3; + } + @counter-style speak-as-extended3 { + system: extends speak-as-extended2; + } + </style> + + <script type="application/javascript"> + + function doTest() { + function testRule(aRule, aNames, aTodo) { + testName(aRule + "-1", aNames[0], null, aTodo); + testName(aRule + "-7", aNames[1], null, aTodo); + testName(aRule + "-29", aNames[2], null, aTodo); + } + + var spellOutNames = ["X. 1", "Y X. 7", "Y Z Y. 29"]; + var bulletsNames = [kDiscBulletText + "1", + kDiscBulletText + "7", + kDiscBulletText + "29"]; + var numbersNames = ["1. 1", "7. 7", "29. 29"]; + var wordsNames = ["one. 1", "seven. 7", "twenty nine. 29"]; + + testRule("system-alphabetic", spellOutNames, true); // bug 1024178 + testRule("system-cyclic", bulletsNames); + testRule("system-numeric", numbersNames); + + testRule("speak-as-bullets", bulletsNames); + testRule("speak-as-numbers", numbersNames); + testRule("speak-as-words", wordsNames); + testRule("speak-as-spell-out", spellOutNames, true); // bug 1024178 + testRule("speak-as-other", wordsNames); + + testRule("speak-as-loop", bulletsNames); + testRule("speak-as-loop0", bulletsNames); + testRule("speak-as-loop1", numbersNames); + + testRule("speak-as-extended0", bulletsNames); + testRule("speak-as-extended1", bulletsNames); + testRule("speak-as-extended2", numbersNames); + testRule("speak-as-extended3", numbersNames); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=966166" + title="Bug 966166 - Implement @counter-style rule"> + Bug 966166 + </a> + + <ol id="list"></ol> + + <script type="application/javascript"> + var list = getNode("list"); + var rules = getNode("counterstyles").sheet.cssRules; + var values = [1, 7, 29]; + for (var i = 0; i < rules.length; i++) { + var rule = rules[i]; + for (var j = 0; j < values.length; j++) { + var item = document.createElement("li"); + item.id = rule.name + "-" + values[j]; + item.value = values[j]; + item.textContent = values[j]; + item.setAttribute("style", "list-style-type: " + rule.name); + list.appendChild(item); + } + } + </script> + +</body> +</html> diff --git a/accessible/tests/mochitest/name/test_general.html b/accessible/tests/mochitest/name/test_general.html new file mode 100644 index 0000000000..1707cff062 --- /dev/null +++ b/accessible/tests/mochitest/name/test_general.html @@ -0,0 +1,706 @@ +<html> + +<head> + <title>nsIAccessible::name calculation</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <script type="application/javascript"> + + function doTest() { + // aria-label + + // Simple label provided via ARIA + testName("btn_simple_aria_label", "I am a button"); + + // aria-label and aria-labelledby, expect aria-labelledby + testName("btn_both_aria_labels", "text I am a button, two"); + + // //////////////////////////////////////////////////////////////////////// + // aria-labelledby + + // Single relation. The value of 'aria-labelledby' contains the ID of + // an element. Gets the name from text node of that element. + testName("btn_labelledby_text", "text"); + + // Multiple relations. The value of 'aria-labelledby' contains the IDs + // of elements. Gets the name from text nodes of those elements. + testName("btn_labelledby_texts", "text1 text2"); + + // //////////////////////////////////////////////////////////////////////// + // Name from named accessible + + testName("input_labelledby_namedacc", "Data"); + + // //////////////////////////////////////////////////////////////////////// + // Name from subtree (single relation labelled_by). + + // Gets the name from text nodes contained by nested elements + testName("btn_labelledby_mixed", "nomore text"); + + // Gets the name from text nodes contained by nested elements, ignores + // hidden elements (bug 443081). + testName("btn_labelledby_mixed_hidden_child", "nomore text2"); + + // Gets the name from hidden text nodes contained by nested elements, + // (label element is hidden entirely), (bug 443081). + testName("btn_labelledby_mixed_hidden", "lala more hidden text"); + + // Gets the name from text nodes contained by nested elements having block + // representation (every text node value in the name should be devided by + // spaces) + testName("btn_labelledby_mixed_block", "text more text"); + + // Gets the name from text nodes contained by html:td (every text node + // value in the name should be devided by spaces). + // XXX: this case is rather a feature than strong wanted behaviour. + testName("btn_labelledby_mixed_table", "text space text"); + + // Gets the name from image accessible. + testName("btn_labelledby_mixed_img", "text image"); + + // Gets the name from input accessibles + // Note: if input have label elements then the name isn't calculated + // from them. + testName("btn_labelledby_mixed_input", + "input button Submit Query Reset Submit Query"); + + // Gets the name from the title of object element. + testName("btn_labelledby_mixed_object", "object"); + + // Gets the name from text nodes. Element br adds space between them. + testName("btn_labelledby_mixed_br", "text text"); + + // Gets the name from label content which allows name from subtree, + // ignore @title attribute on label + testName("from_label_ignoretitle", "Country:"); + + // Gets the name from html:p content, which doesn't allow name from + // subtree, ignore @title attribute on label + testName("from_p_ignoretitle", "Choose country from."); + + // Gets the name from html:input value, ignore @title attribute on input + testName("from_input_ignoretitle", "Custom country"); + + // Insert spaces around the control's value to not jamm sibling text nodes + testName("insert_spaces_around_control", "start value end"); + + // Gets the name from @title, ignore whitespace content + testName("from_label_ignore_ws_subtree", "about"); + + // role="alert" doesn't get name from subtree... + testName("alert", null); + // but the subtree is used if referenced by aria-labelledby. + testName("inputLabelledByAlert", "Error"); + + // //////////////////////////////////////////////////////////////////////// + // label element + + // The label element contains the button. The name is calculated from + // this button. + // Note: the name contains the content of the button. + testName("btn_label_inside", "text10text"); + + // The label element and the button are placed in the same form. Gets + // the name from the label subtree. + testName("btn_label_inform", "in form"); + + // The label element is placed outside of form where the button is. + // Take into account the label. + testName("btn_label_outform", "out form"); + + // The label element and the button are in the same document. Gets the + // name from the label subtree. + testName("btn_label_indocument", "in document"); + + // Multiple label elements for single button + testName("btn_label_multi", "label1label2"); + + // Multiple controls inside a label element + testName("ctrl_in_label_1", "Enable a button control"); + testName("ctrl_in_label_2", "button"); + + + // //////////////////////////////////////////////////////////////////////// + // name from children + + // ARIA role button is presented allowing the name calculation from + // children. + testName("btn_children", "14"); + + // html:button, no name from content + testName("btn_nonamefromcontent", null); + + // ARIA role option is presented allowing the name calculation from + // visible children (bug 443081). + testName("lb_opt1_children_hidden", "i am visible"); + + // Get the name from subtree of menuitem crossing role nothing to get + // the name from its children. + testName("tablemenuitem", "menuitem 1"); + + // Get the name from child acronym title attribute rather than from + // acronym content. + testName("label_with_acronym", "O A T F World Wide Web"); + + testName("testArticle", "Test article"); + + testName("h1", "heading"); + testName("aria_heading", "aria_heading"); + + // //////////////////////////////////////////////////////////////////////// + // title attribute + + // If nothing is left. Let's try title attribute. + testName("btn_title", "title"); + + // //////////////////////////////////////////////////////////////////////// + // textarea name + + // textarea's name should have the value, which initially is specified by + // a text child. + testName("textareawithchild", "Story Foo is ended."); + + // new textarea name should reflect the value change. + var elem = document.getElementById("textareawithchild"); + elem.value = "Bar"; + + testName("textareawithchild", "Story Bar is ended."); + + // //////////////////////////////////////////////////////////////////////// + // controls having a value used as a part of computed name + + testName("ctrlvalue_progressbar:input", "foo 5 baz"); + testName("ctrlvalue_scrollbar:input", "foo 5 baz"); + testName("ctrlvalue_slider:input", "foo 5 baz"); + testName("ctrlvalue_spinbutton:input", "foo 5 baz"); + testName("ctrlvalue_combobox:input", "foo 5 baz"); + + + // /////////////////////////////////////////////////////////////////////// + // label with nested combobox (test for 'f' item of name computation guide) + + testName("comboinstart", "One day(s)."); + testName("combo3", "day(s)."); + + testName("textboxinstart", "Two days."); + testName("textbox1", "days."); + + testName("comboinmiddle", "Subscribe to ATOM feed."); + testName("combo4", "Subscribe to ATOM feed."); + + testName("comboinmiddle2", "Play the Haliluya sound when new mail arrives"); + testName("combo5", null); // label isn't used as a name for control + testName("checkbox", "Play the Haliluya sound when new mail arrives"); + testName("comboinmiddle3", "Play the Haliluya sound when new mail arrives"); + testName("combo6", "Play the Haliluya sound when new mail arrives"); + + testName("comboinend", "This day was sunny"); + testName("combo7", "This day was"); + + testName("textboxinend", "This day was sunny"); + testName("textbox2", "This day was"); + + // placeholder + testName("ph_password", "a placeholder"); + testName("ph_text", "a placeholder"); + testName("ph_textarea", "a placeholder"); + testName("ph_text2", "a label"); + testName("ph_textarea2", "a label"); + testName("ph_text3", "a label"); + + // Test equation image + testName("img_eq", "x^2 + y^2 + z^2"); + testName("input_img_eq", "x^2 + y^2 + z^2"); + testName("txt_eq", "x^2 + y^2 + z^2"); + + // ////////////////////////////////////////////////////////////////////// + // tests for duplicate announcement of content + + testName("test_note", null); + + // ////////////////////////////////////////////////////////////////////// + // Tests for name from sub tree of tr element. + + // By default, we want no name. + testName("NoNameForTR", null); + testName("NoNameForNonStandardTR", null); + + // But we want it if the tr has an ARIA role. + testName("NameForTRBecauseStrongARIA", "a b"); + + // But not if it is a weak (landmark) ARIA role + testName("NoNameForTRBecauseWeakARIA", null); + + // Name from sub tree of grouping if requested by other accessible. + testName("grouping", null); + testName("requested_name_from_grouping", "label"); + testName("listitem_containing_block_tbody", "label"); + // Groupings shouldn't be included when calculating from the subtree of + // a treeitem. + testName("treeitem_containing_grouping", "root"); + + // Name from subtree of grouping labelled by an ancestor. + testName("grouping_labelledby_ancestor", "label"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=428479" + title="Bug 428479 - Support ARIA role=math"> + Bug 428479 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=429666" + title="Expose ROLE_DOCUMENT for ARIA landmarks that inherit from document"> + Bug 429666 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=444279" + title="mochitest for accessible name calculating"> + Bug 444279 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=459635" + title="nsIAccessible::name calculation for HTML buttons"> + Bug 459635 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=530081" + title="Clean up our tree walker"> + Bug 530081 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=604391" + title="Use placeholder as name if name is otherwise empty"> + Bug 604391 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=669312" + title="Accessible name is duplicated when input has a label associated uisng for/id and is wrapped around the input"> + Bug 669312 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=704416" + title="HTML acronym and abbr names should be provided by @title"> + Bug 704416 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=812041" + title="ARIA slider and spinbutton don't provide a value for name computation"> + Bug 812041 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=823927" + title="Text is jammed with control's text in name computation"> + Bug 823927 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=835666" + title="ARIA combobox selected value is not a part of name computation"> + Bug 835666 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=833256" + title="role note shouldn't pick up the name from subtree"> + Mozilla Bug 833256 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- aria-label, simple label --> + <span id="btn_simple_aria_label" role="button" aria-label="I am a button"/> + <br/> + <!-- aria-label plus aria-labelledby --> + <span id="btn_both_aria_labels" role="button" aria-label="I am a button, two" + aria-labelledby="labelledby_text btn_both_aria_labels"/> + <br/> + + <!-- aria-labelledby, single relation --> + <span id="labelledby_text">text</span> + <button id="btn_labelledby_text" + aria-labelledby="labelledby_text">1</button> + <br/> + + <!-- aria-labelledby, multiple relations --> + <span id="labelledby_text1">text1</span> + <span id="labelledby_text2">text2</span> + <button id="btn_labelledby_texts" + aria-labelledby="labelledby_text1 labelledby_text2">2</button> + <br/> + + <!-- name from named accessible --> + <input id="labelledby_namedacc" type="checkbox" + aria-label="Data" /> + <input id="input_labelledby_namedacc" + aria-labelledby="labelledby_namedacc" /> + + <!-- the name from subtree, mixed content --> + <span id="labelledby_mixed">no<span>more text</span></span> + <button id="btn_labelledby_mixed" + aria-labelledby="labelledby_mixed">3</button> + <br/> + + <!-- the name from subtree, mixed/hidden content --> + <span id="labelledby_mixed_hidden_child"> + no<span>more + <span style="display: none;">hidden</span> + text2 + <span style="visibility: hidden">hidden2</span> + </span> + </span> + <button id="btn_labelledby_mixed_hidden_child" + aria-labelledby="labelledby_mixed_hidden_child">3.1</button> + <br/> + + <!-- the name from subtree, mixed/completely hidden content --> + <span id="labelledby_mixed_hidden" + style="display: none;">lala <span>more hidden </span>text</span></span> + <button id="btn_labelledby_mixed_hidden" + aria-labelledby="labelledby_mixed_hidden">3.2</button> + <br/> + + <!-- the name from subtree, mixed content, block structure --> + <div id="labelledby_mixed_block"><div>text</div>more text</div></div> + <button id="btn_labelledby_mixed_block" + aria-labelledby="labelledby_mixed_block">4</button> + <br/> + + <!-- the name from subtree, mixed content, table structure --> + <table><tr> + <td id="labelledby_mixed_table">text<span>space</span>text</td> + </tr></table> + <button id="btn_labelledby_mixed_table" + aria-labelledby="labelledby_mixed_table">5</button> + <br/> + + <!-- the name from subtree, child img --> + <span id="labelledby_mixed_img">text<img alt="image"/></span> + <button id="btn_labelledby_mixed_img" + aria-labelledby="labelledby_mixed_img">6</button> + <br/> + + <!-- the name from subtree, child inputs --> + <span id="labelledby_mixed_input"> + <input type="button" id="input_button" title="input button"/> + <input type="submit" id="input_submit"/> + <input type="reset" id="input_reset"/> + <input type="image" id="input_image" title="input image"/> + </span> + <button id="btn_labelledby_mixed_input" + aria-labelledby="labelledby_mixed_input">7</button> + <br/> + + <!-- the name from subtree, child object --> + <span id="labelledby_mixed_object"> + <object data="about:blank" title="object"></object> + </span> + <button id="btn_labelledby_mixed_object" + aria-labelledby="labelledby_mixed_object">8</button> + <br/> + + <!-- the name from subtree, child br --> + <span id="labelledby_mixed_br">text<br/>text</span> + <button id="btn_labelledby_mixed_br" + aria-labelledby="labelledby_mixed_br">9</button> + <br/> + + <!-- the name from subtree, name from label content rather than from its title + attribute --> + <label for="from_label_ignoretitle" + title="Select your country of origin">Country:</label> + <select id="from_label_ignoretitle"> + <option>Germany</option> + <option>Russia</option> + </select> + + <!-- the name from subtree, name from html:p content rather than from its + title attribute --> + <p id="p_ignoretitle" + title="Select your country of origin">Choose country from.</p> + <select id="from_p_ignoretitle" aria-labelledby="p_ignoretitle"> + <option>Germany</option> + <option>Russia</option> + </select> + + <!-- the name from subtree, name from html:input value rather than from its + title attribute --> + <p id="from_input_ignoretitle" aria-labelledby="input_ignoretitle">Country</p> + <input id="input_ignoretitle" + value="Custom country" + title="Input your country of origin"/ > + + <!-- name from subtree, surround control by spaces to not jamm the text --> + <label id="insert_spaces_around_control"> + start<input value="value">end + </label> + + <!-- no name from subtree because it holds whitespaces only --> + <a id="from_label_ignore_ws_subtree" href="about:mozilla" title="about"> </a> + + <!-- Don't use subtree unless referenced by aria-labelledby. --> + <div id="alert" role="alert">Error</div> + <input type="text" id="inputLabelledByAlert" aria-labelledby="alert"> + + <!-- label element, label contains control --> + <label>text<button id="btn_label_inside">10</button>text</label> + <br/> + + <!-- label element, label and the button are in the same form --> + <form> + <label for="btn_label_inform">in form</label> + <button id="btn_label_inform">11</button> + </form> + + <!-- label element, label is outside of the form of the button --> + <label for="btn_label_outform">out form</label> + <form> + <button id="btn_label_outform">12</button> + </form> + + <!-- label element, label and the button are in the same document --> + <label for="btn_label_indocument">in document</label> + <button id="btn_label_indocument">13</button> + + <!-- multiple label elements for single button --> + <label for="btn_label_multi">label1</label> + <label for="btn_label_multi">label2</label> + <button id="btn_label_multi">button</button> + + <!-- a label containing more than one controls --> + <label> + Enable <input id="ctrl_in_label_1" type="checkbox"> a + <input id="ctrl_in_label_2" type="button" value="button"> control + </label> + + <!-- name from children --> + <span id="btn_children" role="button">14</span> + + <!-- no name from content, ARIA role overrides this rule --> + <button id="btn_nonamefromcontent" role="img">1</button> + + <!-- name from children, hidden children --> + <div role="listbox" tabindex="0"> + <div id="lb_opt1_children_hidden" role="option" tabindex="0"> + <span>i am visible</span> + <span style="display:none">i am hidden</span> + </div> + </div> + + <table role="menu"> + <tr role="menuitem" id="tablemenuitem"> + <td>menuitem 1</td> + </tr> + <tr role="menuitem"> + <td>menuitem 2</td> + </tr> + </table> + + <label id="label_with_acronym"> + <acronym title="O A T F">OATF</acronym> + <abbr title="World Wide Web">WWW</abbr> + </label> + + <div id="testArticle" role="article" title="Test article"> + <p>This is a paragraph inside the article.</p> + </div> + + <h1 id="h1" title="oops">heading</h1> + <div role="heading" id="aria_heading">aria_heading</div> + + <!-- name from title attribute --> + <span id="btn_title" role="group" title="title">15</span> + + <!-- A textarea nested in a label with a text child (bug #453371). --> + <form> + <label>Story + <textarea id="textareawithchild" name="name">Foo</textarea> + is ended. + </label> + </form> + + <!-- controls having a value used as part of computed name --> + <input type="checkbox" id="ctrlvalue_progressbar:input"> + <label for="ctrlvalue_progressbar:input"> + foo <span role="progressbar" + aria-valuenow="5" aria-valuemin="1" + aria-valuemax="10">5</span> baz + </label> + + <input type="checkbox" id="ctrlvalue_scrollbar:input" /> + <label for="ctrlvalue_scrollbar:input"> + foo <span role="scrollbar" + aria-valuenow="5" aria-valuemin="1" + aria-valuemax="10">5</span> baz + </label> + + <input type="checkbox" id="ctrlvalue_slider:input"> + <label for="ctrlvalue_slider:input"> + foo <input role="slider" type="range" + value="5" min="1" max="10" + aria-valuenow="5" aria-valuemin="1" + aria-valuemax="10"> baz + </label> + + <input type="checkbox" id="ctrlvalue_spinbutton:input"> + <label for="ctrlvalue_spinbutton:input"> + foo <input role="spinbutton" type="number" + value="5" min="1" max="10" + aria-valuenow="5" aria-valuemin="1" + aria-valuemax="10"> + baz + </label> + + <input type="checkbox" id="ctrlvalue_combobox:input"> + <label for="ctrlvalue_combobox:input"> + foo + <div role="combobox"> + <div role="textbox"></div> + <ul role="listbox" style="list-style-type: none;"> + <li role="option">1</li> + <li role="option" aria-selected="true">5</li> + <li role="option">3</li> + </ul> + </div> + baz + </label> + + <!-- a label with a nested control in the start, middle and end --> + <form> + <!-- at the start (without and with whitespaces) --> + <label id="comboinstart"><select id="combo3"> + <option>One</option> + <option>Two</option> + </select> + day(s). + </label> + + <label id="textboxinstart"> + <input id="textbox1" value="Two"> + days. + </label> + + <!-- in the middle --> + <label id="comboinmiddle"> + Subscribe to + <select id="combo4" name="occupation"> + <option>ATOM</option> + <option>RSS</option> + </select> + feed. + </label> + + <label id="comboinmiddle2" for="checkbox">Play the + <select id="combo5"> + <option>Haliluya</option> + <option>Hurra</option> + </select> + sound when new mail arrives + </label> + <input id="checkbox" type="checkbox" /> + + <label id="comboinmiddle3" for="combo6">Play the + <select id="combo6"> + <option>Haliluya</option> + <option>Hurra</option> + </select> + sound when new mail arrives + </label> + + <!-- at the end (without and with whitespaces) --> + <label id="comboinend"> + This day was + <select id="combo7" name="occupation"> + <option>sunny</option> + <option>rainy</option> + </select></label> + + <label id="textboxinend"> + This day was + <input id="textbox2" value="sunny"> + </label> + </form> + + <!-- placeholder --> + <input id="ph_password" type="password" value="" placeholder="a placeholder" /> + <input id="ph_text" type="text" placeholder="a placeholder" /> + <textarea id="ph_textarea" cols="5" placeholder="a placeholder"></textarea> + + <!-- placeholder does not win --> + <input id="ph_text2" type="text" aria-label="a label" placeholder="meh" /> + <textarea id="ph_textarea2" cols="5" aria-labelledby="ph_text2" + placeholder="meh"></textarea> + + <label for="ph_text3">a label</label> + <input id="ph_text3" placeholder="meh" /> + + <p>Image: + <img id="img_eq" role="math" src="foo" alt="x^2 + y^2 + z^2"> + <input type="image" id="input_img_eq" src="foo" alt="x^2 + y^2 + z^2"> + </p> + + <p>Text: + <span id="txt_eq" role="math" title="x^2 + y^2 + z^2">x<sup>2</sup> + + y<sup>2</sup> + z<sup>2</sup></span> + + <!-- duplicate announcement --> + <div id="test_note" role="note">subtree</div> + + <!-- No name for tr from its sub tree --> + <table><tr id="NoNameForTR"><th>a</th><td>b</td></tr></table> + <table style="display: block;"> + <tr id="NoNameForNonStandardTR" style="display:block;"> + <th>a</th><td>b</td> + </tr> + </table> + + <!-- Name from sub tree of tr, because it has a strong ARIA role --> + <table><tr id="NameForTRBecauseStrongARIA" role="row"><th>a</th><td>b</td></tr></table> + + <!-- No name for tr because of weak (landmark) role --> + <table><tr id="NoNameForTRBecauseWeakARIA" role="main"><th>a</th><td>b</td></tr></table> + + <!-- Name from subtree of grouping if requested by other object --> + <div id="grouping" role="group">label</div> + <button id="requested_name_from_grouping"aria-labelledby="grouping"></button> + <!-- Name from sub tree of tbody marked as display:block;, which is also a grouping --> + <div id="listitem_containing_block_tbody" role="listitem"> + <table> + <tbody style="display: block;"> + <tr><td>label</td></tr> + </tbody> + </table> + </div> + <!-- Name from subtree of treeitem containing grouping --> + <div id="treeitem_containing_grouping" role="treeitem" aria-level="1" aria-expanded="true">root + <div role="group"> + <div role="treeitem" aria-level="2">sub</div> + </div> + </div> + + <!-- Name from subtree of grouping labelled by an ancestor. --> + <div id="grouping_ancestor_label">label + <div id="grouping_labelledby_ancestor" role="group" aria-labelledby="grouping_ancestor_label"> + This content should not be included in the grouping's label. + </div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/name/test_general.xhtml b/accessible/tests/mochitest/name/test_general.xhtml new file mode 100644 index 0000000000..2d8abaf4d5 --- /dev/null +++ b/accessible/tests/mochitest/name/test_general.xhtml @@ -0,0 +1,339 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Accessibility Name Calculating Test."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <script type="application/javascript"> + <![CDATA[ + function doTest() + { + // aria-label + + // Simple label provided via ARIA + testName("btn_simple_aria_label", "I am a button"); + + // aria-label and aria-labelledby, expect aria-labelledby + testName("btn_both_aria_labels", "text I am a button, two"); + + ////////////////////////////////////////////////////////////////////////// + // aria-labelledby + + // Single relation. The value of 'aria-labelledby' contains the ID of + // an element. Gets the name from text node of that element. + testName("btn_labelledby_text", "text"); + + // Multiple relations. The value of 'aria-labelledby' contains the IDs + // of elements. Gets the name from text nodes of those elements. + testName("btn_labelledby_texts", "text1 text2"); + + // Trick cases. Self and recursive referencing. + testName("rememberHistoryDays", "Remember 3 days"); + testName("historyDays", "Remember 3 days"); + testName("rememberAfter", "days"); + + ////////////////////////////////////////////////////////////////////////// + // Name from subtree (single relation labelled_by). + + // Gets the name from text nodes contained by nested elements. + testName("btn_labelledby_mixed", "no more text"); + + // Gets the name from text nodes and selected item of menulist + // (other items are ignored). + testName("btn_labelledby_mixed_menulist", + "no more text selected item more text"); + + // Gets the name from text nodes contained by nested elements, ignores + // hidden elements (bug 443081). + testName("btn_labelledby_mixed_hidden_child", "no more text2"); + + // Gets the name from hidden text nodes contained by nested elements, + // (label element is hidden entirely), (bug 443081) + testName("btn_labelledby_mixed_hidden", "lala more hidden text"); + + + ////////////////////////////////////////////////////////////////////////// + // Name from @label attribute. + + // Gets the name from @label attribute. + testName("btn_labelattr", "labeled element"); + + + ////////////////////////////////////////////////////////////////////////// + // Name for nsIDOMXULSelectControlItemElement. + + // Gets the name from @label attribute. + testName("li_nsIDOMXULSelectControlItemElement", "select control item"); + + + ////////////////////////////////////////////////////////////////////////// + // Name if the XUL element doesn't implement nsIDOMXULSelectControlElement + // and has @label attribute. + + testName("box_not_nsIDOMXULSelectControlElement", "box"); + + + ////////////////////////////////////////////////////////////////////////// + // Name from the label element. + + // The label and button are placed on 2nd level relative common parent. + testName("btn_label_1", "label1"); + + // The label is on 1st, the button is on 5th level relative common parent. + testName("btn_label_2", "label2"); + + // The label and button are siblings. + testName("btn_label_3", "label3"); + + // Multiple labels for single button: XUL button takes the last one. + testName("btn_label_4", "label5"); + + // Label associated with HTML element. + testName("input_label", "input label"); + + + ////////////////////////////////////////////////////////////////////////// + // tooltiptext (if nothing above isn't presented then tooltiptext is used) + testName("box_tooltiptext", "tooltiptext label"); + + + ////////////////////////////////////////////////////////////////////////// + // Name from the @title attribute of <toolbaritem/> (original bug 237249). + + // Direct child of toolbaritem. + testName("toolbaritem_child", null); + + // Child from subtree of toolbaritem. + testName("toolbaritem_hboxbutton", "button"); + + + ////////////////////////////////////////////////////////////////////////// + // name from label inside toolbar button + testName("toolbarbuttonwithlabel", "I am the button"); + + + ////////////////////////////////////////////////////////////////////////// + // Name from children + + // ARIA role button is presented allowing the name calculation from + // children. + testName("box_children", "14"); + + // ARIA role option is presented allowing the name calculation from + // the visible children (bug 443081) + testName("lb_opt1_children_hidden", "i am visible"); + + + ////////////////////////////////////////////////////////////////////////// + // Name from aria-labelledby: menuitem label+ listitem label + testName("li_labelledby", "Show an Alert The moment the event starts"); + + ////////////////////////////////////////////////////////////////////////// + // groupbox labeling from first label + testName("groupbox", "Some caption"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=444279" + title="mochitest for accessible name calculating"> + Mozilla Bug 444279 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=441991" + title="nsXULListitemAccessible::GetName prefers label \ + attribute over aria-labelledby and doesn't allow recursion"> + Mozilla Bug 441991 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + + <!-- aria-label, simple label --> + <button id="btn_simple_aria_label" aria-label="I am a button"/> + + <!-- aria-label plus aria-labelledby --> + <button id="btn_both_aria_labels" aria-label="I am a button, two" + aria-labelledby="labelledby_text btn_both_aria_labels"/> + + <!-- aria-labelledby, single relation --> + <description id="labelledby_text">text</description> + <button id="btn_labelledby_text" + aria-labelledby="labelledby_text"/> + + <!-- aria-labelledby, multiple relations --> + <description id="labelledby_text1">text1</description> + <description id="labelledby_text2">text2</description> + <button id="btn_labelledby_texts" + aria-labelledby="labelledby_text1 labelledby_text2"/> + + <!-- trick aria-labelledby --> + <checkbox id="rememberHistoryDays" + label="Remember " + aria-labelledby="rememberHistoryDays historyDays rememberAfter"/> + <html:input id="historyDays" value="3" + aria-labelledby="rememberHistoryDays historyDays rememberAfter"/> + <label id="rememberAfter">days</label> + + <!-- the name from subtree, mixed content --> + <description id="labelledby_mixed"> + no<description>more text</description> + </description> + <button id="btn_labelledby_mixed" + aria-labelledby="labelledby_mixed"/> + + <!-- the name from subtree, mixed/hidden content --> + <description id="labelledby_mixed_hidden_child">no<description>more <description hidden="true">hidden</description>text2</description></description> + <button id="btn_labelledby_mixed_hidden_child" + aria-labelledby="labelledby_mixed_hidden_child"/> + + <!-- the name from subtree, mixed/completely hidden content --> + <description id="labelledby_mixed_hidden" + hidden="true">lala <description>more hidden </description>text</description> + <button id="btn_labelledby_mixed_hidden" + aria-labelledby="labelledby_mixed_hidden"/> + <br/> + + <!-- the name from subtree, mixed content, ignore items of menulist --> + <description id="labelledby_mixed_menulist"> + no<description>more text</description> + <menulist> + <menupopup> + <menuitem label="selected item"/> + <menuitem label="item"/> + </menupopup> + </menulist> + more text + </description> + <button id="btn_labelledby_mixed_menulist" + aria-labelledby="labelledby_mixed_menulist"/> + + <!-- @label --> + <button id="btn_labelattr" + label="labeled element"/> + + <!-- nsIDOMXULSelectControlItemElement --> + <richlistbox> + <richlistitem id="li_nsIDOMXULSelectControlItemElement"> + <label value="select control item"/> + </richlistitem> + </richlistbox> + + <!-- not nsIDOMXULSelectControlElement --> + <box id="box_not_nsIDOMXULSelectControlElement" role="group" label="box"/> + + <!-- label element --> + <hbox> + <box> + <label control="btn_label_1">label1</label> + </box> + <label control="btn_label_2">label2</label> + <box> + <button id="btn_label_1"/> + <box> + <box> + <box> + <button id="btn_label_2"/> + </box> + </box> + </box> + </box> + <label control="btn_label_3">label3</label> + <button id="btn_label_3"/> + + <label control="btn_label_4">label4</label> + <label control="btn_label_4">label5</label> + <button id="btn_label_4"/> + + <label control="input_label">input label</label> + <html:input id="input_label"/> + </hbox> + + <!-- tooltiptext --> + <box id="box_tooltiptext" + role="group" + tooltiptext="tooltiptext label"/> + + <!-- the name from @title of toolbaritem --> + <!-- and the name from label of a toolbarbutton --> + <toolbar> + <toolbaritem title="ooospspss"> + <box id="toolbaritem_child" + role="group" + flex="1"> + <hbox role="button" id="toolbaritem_hboxbutton"> + <description value="button"/> + </hbox> + </box> + </toolbaritem> + <toolbarbutton id="toolbarbuttonwithlabel"> + <label flex="1">I am the button</label> + </toolbarbutton> + </toolbar> + + <!-- name from children --> + <box id="box_children" role="button">14</box> + + <!-- name from children, hidden children --> + <vbox role="listbox" tabindex="0"> + <hbox id="lb_opt1_children_hidden" role="option" tabindex="0"> + <description>i am visible</description> + <description style="display:none">i am hidden</description> + </hbox> + + <!-- Name from caption sub tree --> + <groupbox id="groupbox"> + <label>Some caption</label> + <checkbox label="some checkbox label" /> + </groupbox> + </vbox> + + <!-- bug 441991; create name from other menuitem label listitem's own label --> + <hbox> + <richlistbox> + <richlistitem id="li_labelledby" + aria-labelledby="menuitem-DISPLAY li_labelledby"> + <label value="The moment the event starts"/> + </richlistitem> + </richlistbox> + <menulist> + <menupopup> + <menuitem id="menuitem-DISPLAY" + value="DISPLAY" + label="Show an Alert"/> + <menuitem id="menuitem-EMAIL" + value="EMAIL" + label="Send an E-mail"/> + </menupopup> + </menulist> + </hbox> + + </vbox> <!-- close tests area --> + </hbox> <!-- close main area --> +</window> diff --git a/accessible/tests/mochitest/name/test_link.html b/accessible/tests/mochitest/name/test_link.html new file mode 100644 index 0000000000..6a289dd44f --- /dev/null +++ b/accessible/tests/mochitest/name/test_link.html @@ -0,0 +1,87 @@ +<html> + +<head> + <title>nsIAccessible::name calculation for HTML links (html:a)</title> + + <link rel="stylesheet" + type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <script type="application/javascript"> + function doTest() { + // aria-label + testName("aria_label", "anchor label"); + + // aria-labelledby + testName("aria_labelledby", "text"); + + // name from content + testName("namefromcontent", "1"); + + // name from content + testName("namefromimg", "img title"); + + // no name from content + testName("nonamefromcontent", null); + + // title + testName("title", "title"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=459782" + title="nsIAccessible::name calculation for HTML links (html:a)"> + Mozilla Bug 459782 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- aria-label --> + <a id="aria_label" href="mozilla.org" + aria-label="anchor label">1</a> + <br/> + + <!-- aria-labelledby, preferred to html:label --> + <span id="text">text</span> + <label for="aria_labelledby">label</label> + <a id="aria_labelledby" href="mozilla.org" + aria-labelledby="text">1</a> + <br/> + + <!-- name from content, preferred to @title --> + <a id="namefromcontent" href="mozilla.org" + title="title">1</a> + <br/> + + <!-- name from content, preferred to @title --> + <a id="namefromimg" href="mozilla.org" + title="title"><img alt="img title" /></a> + + <!-- no name from content, ARIA role overrides this rule --> + <a id="nonamefromcontent" href="mozilla.org" role="img">1</a> + <br/> + + <!-- no content, name from @title --> + <a id="title" href="mozilla.org" + title="title"></a> + +</body> +</html> diff --git a/accessible/tests/mochitest/name/test_list.html b/accessible/tests/mochitest/name/test_list.html new file mode 100644 index 0000000000..86afef66b2 --- /dev/null +++ b/accessible/tests/mochitest/name/test_list.html @@ -0,0 +1,89 @@ +<html> + +<head> + <title>nsIAccessible::name calculation for HTML li</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../name.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Alter list item numbering and change list style type. + */ + function bulletUpdate() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getNode("list")), + ]; + + this.invoke = function bulletUpdate_invoke() { + testName("li_end", "1. list end"); + + var li = document.createElement("li"); + li.setAttribute("id", "li_start"); + li.textContent = "list start"; + getNode("list").insertBefore(li, getNode("li_end")); + }; + + this.finalCheck = function bulletUpdate_finalCheck() { + testName("li_start", "1. list start"); + testName("li_end", "2. list end"); + + // change list style type + var list = getNode("list"); + list.setAttribute("style", "list-style-type: disc;"); + + // Flush both the style change and the resulting layout change. + // Flushing style on its own is not sufficient, because that can + // leave frames marked with NS_FRAME_IS_DIRTY, which will cause + // nsTextFrame::GetRenderedText to report the text of a text + // frame is empty. + list.offsetWidth; // flush layout (which also flushes style) + + testName("li_start", kDiscBulletText + "list start"); + testName("li_end", kDiscBulletText + "list end"); + }; + + this.getID = function bulletUpdate_getID() { + return "Update bullet of list items"; + }; + } + + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + gQueue.push(new bulletUpdate()); + gQueue.invoke(); // SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=634200" + title="crash [@ nsIFrame::StyleVisibility() ]"> + Mozilla Bug 634200 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <ol id="list"> + <li id="li_end">list end</li> + </ol> + +</body> +</html> diff --git a/accessible/tests/mochitest/name/test_markup.html b/accessible/tests/mochitest/name/test_markup.html new file mode 100644 index 0000000000..735027f44f --- /dev/null +++ b/accessible/tests/mochitest/name/test_markup.html @@ -0,0 +1,58 @@ +<html> + +<head> + <title>nsIAccessible::name calculation for elements</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../name.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript" + src="markup.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpID = "eventdump"; + // gDumpToConsole = true; + // gA11yEventDumpToConsole = true; + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(testNames); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=459635" + title="nsIAccessible::name calculation for elements"> + Bug 459635 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=666212" + title="summary attribute content mapped to accessible name in MSAA"> + Bug 666212 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=786163" + title=" Sort out name calculation for HTML input buttons"> + Bug 786163 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/name/test_svg.html b/accessible/tests/mochitest/name/test_svg.html new file mode 100644 index 0000000000..535fcdbf20 --- /dev/null +++ b/accessible/tests/mochitest/name/test_svg.html @@ -0,0 +1,53 @@ +<html> + +<head> + <title>Accessible name and description for SVG elements</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../name.js"></script> + + <script type="application/javascript"> + + function doTest() { + testName("svg1", "A name"); + testDescr("svg1", "A description"); + testName("svg2", "A tooltip"); + testDescr("svg2", ""); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=459357" + title="Support accessible name computation for SVG"> + Mozilla Bug 459357 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg1"> + <title>A name</title> + <desc>A description</title> + </svg> + + <svg xmlns="http://www.w3.org/2000/svg" version="1.1" id="svg2"> + <desc>A tooltip</desc> + </svg> +</body> +</html> diff --git a/accessible/tests/mochitest/name/test_tree.xhtml b/accessible/tests/mochitest/name/test_tree.xhtml new file mode 100644 index 0000000000..3564481d00 --- /dev/null +++ b/accessible/tests/mochitest/name/test_tree.xhtml @@ -0,0 +1,207 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessibility Name Calculating Test."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../name.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + <![CDATA[ + function treeTester(aID) + { + this.DOMNode = getNode(aID); + + this.invoke = function treeTester_invoke() + { + this.DOMNode.view = new nsTreeTreeView(); + } + + this.check = function treeTester_check(aEvent) + { + var tree = { + role: ROLE_OUTLINE, + children: [ + { + role: ROLE_LIST + }, + { + role: ROLE_OUTLINEITEM, + children: [], + name: "row1col" + }, + { + role: ROLE_OUTLINEITEM, + children: [], + name: "row2_col" + }, + { + role: ROLE_OUTLINEITEM, + children: [], + name: "row2.1_col" + }, + { + role: ROLE_OUTLINEITEM, + children: [], + name: "row2.2_col" + }, + { + role: ROLE_OUTLINEITEM, + children: [], + name: "row3_col" + }, + { + role: ROLE_OUTLINEITEM, + children: [], + name: "row4col" + } + ] + }; + testAccessibleTree(this.DOMNode, tree); + } + + this.getID = function treeTester_getID() + { + return "Tree name testing for " + aID; + } + } + + function tableTester(aID, aIsTable, aCol1ID, aCol2ID) + { + this.DOMNode = getNode(aID); + + this.invoke = function tableTester_invoke() + { + this.DOMNode.view = new nsTableTreeView(2); + } + + this.check = function tableTester_check(aEvent) + { + var tree = { + role: aIsTable ? ROLE_TABLE : ROLE_TREE_TABLE, + children: [ + { + role: ROLE_LIST + }, + { + role: ROLE_ROW, + children: [ + { + role: ROLE_GRID_CELL, + children: [], + name: "row0_" + aCol1ID + }, + { + role: ROLE_GRID_CELL, + children: [], + name: "row0_" + aCol2ID + } + ], + name: "row0_" + aCol1ID + " row0_" + aCol2ID + }, + { + role: ROLE_ROW, + children: [ + { + role: ROLE_GRID_CELL, + children: [], + name: "row1_" + aCol1ID + }, + { + role: ROLE_GRID_CELL, + children: [], + name: "row1_" + aCol2ID + } + ], + name: "row1_" + aCol1ID + " row1_" + aCol2ID + } + ] + }; + testAccessibleTree(this.DOMNode, tree); + } + + this.getID = function tableTester_getID() + { + return "Tree name testing for " + aID; + } + } + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(EVENT_REORDER); + + gQueue.push(new treeTester("tree")); + gQueue.push(new tableTester("table", true, "t_col1", "t_col2")); + gQueue.push(new tableTester("treetable", false, "tt_col1", "tt_col2")); + + gQueue.invoke(); // Will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=546812" + title="Treegrid row accessible shouldn't inherit name from tree accessible"> + Mozilla Bug 546812 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=664376" + title="Table rows of XUL trees no longer containing cell content as accessible name"> + Mozilla Bug 664376 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <tree id="table" flex="1"> + <treecols> + <treecol id="t_col1" flex="1" label="column"/> + <treecol id="t_col2" flex="1" label="column 2"/> + </treecols> + <treechildren/> + </tree> + + <tree id="treetable" flex="1"> + <treecols> + <treecol id="tt_col1" flex="1" label="column" primary="true"/> + <treecol id="tt_col2" flex="1" label="column 2"/> + </treecols> + <treechildren/> + </tree> + + </vbox> <!-- close tests area --> + </hbox> <!-- close main area --> +</window> diff --git a/accessible/tests/mochitest/pivot.js b/accessible/tests/mochitest/pivot.js new file mode 100644 index 0000000000..e253405e06 --- /dev/null +++ b/accessible/tests/mochitest/pivot.js @@ -0,0 +1,665 @@ +/* import-globals-from common.js */ +/* import-globals-from events.js */ +/* import-globals-from role.js */ +/* import-globals-from states.js */ +/* import-globals-from text.js */ + +ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm"); + +// ////////////////////////////////////////////////////////////////////////////// +// Constants + +const PREFILTER_INVISIBLE = nsIAccessibleTraversalRule.PREFILTER_INVISIBLE; +const PREFILTER_TRANSPARENT = nsIAccessibleTraversalRule.PREFILTER_TRANSPARENT; +const FILTER_MATCH = nsIAccessibleTraversalRule.FILTER_MATCH; +const FILTER_IGNORE = nsIAccessibleTraversalRule.FILTER_IGNORE; +const FILTER_IGNORE_SUBTREE = nsIAccessibleTraversalRule.FILTER_IGNORE_SUBTREE; +const NO_BOUNDARY = nsIAccessiblePivot.NO_BOUNDARY; +const CHAR_BOUNDARY = nsIAccessiblePivot.CHAR_BOUNDARY; +const WORD_BOUNDARY = nsIAccessiblePivot.WORD_BOUNDARY; + +const NS_ERROR_NOT_IN_TREE = 0x80780026; +const NS_ERROR_INVALID_ARG = 0x80070057; + +// ////////////////////////////////////////////////////////////////////////////// +// Traversal rules + +/** + * Rule object to traverse all focusable nodes and text nodes. + */ +var HeadersTraversalRule = { + getMatchRoles() { + return [ROLE_HEADING]; + }, + + preFilter: PREFILTER_INVISIBLE, + + match(aAccessible) { + return FILTER_MATCH; + }, + + QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]), +}; + +/** + * Traversal rule for all focusable nodes or leafs. + */ +var ObjectTraversalRule = { + getMatchRoles() { + return []; + }, + + preFilter: PREFILTER_INVISIBLE | PREFILTER_TRANSPARENT, + + match(aAccessible) { + var rv = FILTER_IGNORE; + var role = aAccessible.role; + if ( + hasState(aAccessible, STATE_FOCUSABLE) && + role != ROLE_DOCUMENT && + role != ROLE_INTERNAL_FRAME + ) { + rv = FILTER_IGNORE_SUBTREE | FILTER_MATCH; + } else if ( + aAccessible.childCount == 0 && + role != ROLE_LISTITEM_MARKER && + aAccessible.name.trim() + ) { + rv = FILTER_MATCH; + } + + return rv; + }, + + QueryInterface: ChromeUtils.generateQI([nsIAccessibleTraversalRule]), +}; + +// ////////////////////////////////////////////////////////////////////////////// +// Virtual state invokers and checkers + +/** + * A checker for virtual cursor changed events. + */ +function VCChangedChecker( + aDocAcc, + aIdOrNameOrAcc, + aTextOffsets, + aPivotMoveMethod, + aIsFromUserInput, + aBoundaryType = NO_BOUNDARY +) { + this.__proto__ = new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc); + + this.match = function VCChangedChecker_match(aEvent) { + var event = null; + try { + event = aEvent.QueryInterface(nsIAccessibleVirtualCursorChangeEvent); + } catch (e) { + return false; + } + + var expectedReason = + VCChangedChecker.methodReasonMap[aPivotMoveMethod] || + nsIAccessiblePivot.REASON_NONE; + + return ( + event.reason == expectedReason && event.boundaryType == aBoundaryType + ); + }; + + this.check = function VCChangedChecker_check(aEvent) { + SimpleTest.info("VCChangedChecker_check"); + + var event = null; + try { + event = aEvent.QueryInterface(nsIAccessibleVirtualCursorChangeEvent); + } catch (e) { + SimpleTest.ok(false, "Does not support correct interface: " + e); + } + + var position = aDocAcc.virtualCursor.position; + var idMatches = position && position.DOMNode.id == aIdOrNameOrAcc; + var nameMatches = position && position.name == aIdOrNameOrAcc; + var accMatches = position == aIdOrNameOrAcc; + + SimpleTest.ok( + idMatches || nameMatches || accMatches, + "id or name matches - expecting " + + prettyName(aIdOrNameOrAcc) + + ", got '" + + prettyName(position) + ); + + SimpleTest.is( + aEvent.isFromUserInput, + aIsFromUserInput, + "Expected user input is " + aIsFromUserInput + "\n" + ); + + SimpleTest.is( + event.newAccessible, + position, + "new position in event is incorrect" + ); + + if (aTextOffsets) { + SimpleTest.is( + aDocAcc.virtualCursor.startOffset, + aTextOffsets[0], + "wrong start offset" + ); + SimpleTest.is( + aDocAcc.virtualCursor.endOffset, + aTextOffsets[1], + "wrong end offset" + ); + SimpleTest.is( + event.newStartOffset, + aTextOffsets[0], + "wrong start offset in event" + ); + SimpleTest.is( + event.newEndOffset, + aTextOffsets[1], + "wrong end offset in event" + ); + } + + var prevPosAndOffset = VCChangedChecker.getPreviousPosAndOffset( + aDocAcc.virtualCursor + ); + + if (prevPosAndOffset) { + SimpleTest.is( + event.oldAccessible, + prevPosAndOffset.position, + "previous position does not match" + ); + SimpleTest.is( + event.oldStartOffset, + prevPosAndOffset.startOffset, + "previous start offset does not match" + ); + SimpleTest.is( + event.oldEndOffset, + prevPosAndOffset.endOffset, + "previous end offset does not match" + ); + } + }; +} + +VCChangedChecker.prevPosAndOffset = {}; + +VCChangedChecker.storePreviousPosAndOffset = function storePreviousPosAndOffset( + aPivot +) { + VCChangedChecker.prevPosAndOffset[aPivot] = { + position: aPivot.position, + startOffset: aPivot.startOffset, + endOffset: aPivot.endOffset, + }; +}; + +VCChangedChecker.getPreviousPosAndOffset = function getPreviousPosAndOffset( + aPivot +) { + return VCChangedChecker.prevPosAndOffset[aPivot]; +}; + +VCChangedChecker.methodReasonMap = { + moveNext: nsIAccessiblePivot.REASON_NEXT, + movePrevious: nsIAccessiblePivot.REASON_PREV, + moveFirst: nsIAccessiblePivot.REASON_FIRST, + moveLast: nsIAccessiblePivot.REASON_LAST, + setTextRange: nsIAccessiblePivot.REASON_NONE, + moveNextByText: nsIAccessiblePivot.REASON_NEXT, + movePreviousByText: nsIAccessiblePivot.REASON_PREV, + moveToPoint: nsIAccessiblePivot.REASON_POINT, +}; + +/** + * Set a text range in the pivot and wait for virtual cursor change event. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aTextAccessible [in] accessible to set to virtual cursor's position + * @param aTextOffsets [in] start and end offsets of text range to set in + * virtual cursor. + */ +function setVCRangeInvoker(aDocAcc, aTextAccessible, aTextOffsets) { + this.invoke = function virtualCursorChangedInvoker_invoke() { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + SimpleTest.info(prettyName(aTextAccessible) + " " + aTextOffsets); + aDocAcc.virtualCursor.setTextRange( + aTextAccessible, + aTextOffsets[0], + aTextOffsets[1] + ); + }; + + this.getID = function setVCRangeInvoker_getID() { + return ( + "Set offset in " + + prettyName(aTextAccessible) + + " to (" + + aTextOffsets[0] + + ", " + + aTextOffsets[1] + + ")" + ); + }; + + this.eventSeq = [ + new VCChangedChecker( + aDocAcc, + aTextAccessible, + aTextOffsets, + "setTextRange", + true + ), + ]; +} + +/** + * Move the pivot and wait for virtual cursor change event. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) + * @param aRule [in] traversal rule object + * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect + * virtual cursor to land on after performing move method. + * false if no move is expected. + * @param aIsFromUserInput [in] set user input flag when invoking method, and + * expect it in the event. + */ +function setVCPosInvoker( + aDocAcc, + aPivotMoveMethod, + aRule, + aIdOrNameOrAcc, + aIsFromUserInput +) { + // eslint-disable-next-line mozilla/no-compare-against-boolean-literals + var expectMove = aIdOrNameOrAcc != false; + this.invoke = function virtualCursorChangedInvoker_invoke() { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + if (aPivotMoveMethod && aRule) { + var moved = false; + switch (aPivotMoveMethod) { + case "moveFirst": + case "moveLast": + moved = aDocAcc.virtualCursor[aPivotMoveMethod]( + aRule, + aIsFromUserInput === undefined ? true : aIsFromUserInput + ); + break; + case "moveNext": + case "movePrevious": + moved = aDocAcc.virtualCursor[aPivotMoveMethod]( + aRule, + aDocAcc.virtualCursor.position, + false, + aIsFromUserInput === undefined ? true : aIsFromUserInput + ); + break; + } + SimpleTest.is( + !!moved, + !!expectMove, + "moved pivot with " + aPivotMoveMethod + " to " + aIdOrNameOrAcc + ); + } else { + aDocAcc.virtualCursor.position = getAccessible(aIdOrNameOrAcc); + } + }; + + this.getID = function setVCPosInvoker_getID() { + return "Do " + (expectMove ? "" : "no-op ") + aPivotMoveMethod; + }; + + if (expectMove) { + this.eventSeq = [ + new VCChangedChecker( + aDocAcc, + aIdOrNameOrAcc, + null, + aPivotMoveMethod, + aIsFromUserInput === undefined ? !!aPivotMoveMethod : aIsFromUserInput + ), + ]; + } else { + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc), + ]; + } +} + +/** + * Move the pivot by text and wait for virtual cursor change event. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPivotMoveMethod [in] method to test (ie. "moveNext", "moveFirst", etc.) + * @param aBoundary [in] boundary constant + * @param aTextOffsets [in] start and end offsets of text range to set in + * virtual cursor. + * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect + * virtual cursor to land on after performing move method. + * false if no move is expected. + * @param aIsFromUserInput [in] set user input flag when invoking method, and + * expect it in the event. + */ +function setVCTextInvoker( + aDocAcc, + aPivotMoveMethod, + aBoundary, + aTextOffsets, + aIdOrNameOrAcc, + aIsFromUserInput +) { + // eslint-disable-next-line mozilla/no-compare-against-boolean-literals + var expectMove = aIdOrNameOrAcc != false; + this.invoke = function virtualCursorChangedInvoker_invoke() { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + SimpleTest.info(aDocAcc.virtualCursor.position); + var moved = aDocAcc.virtualCursor[aPivotMoveMethod]( + aBoundary, + aIsFromUserInput === undefined + ); + SimpleTest.is( + !!moved, + !!expectMove, + "moved pivot by text with " + aPivotMoveMethod + " to " + aIdOrNameOrAcc + ); + }; + + this.getID = function setVCPosInvoker_getID() { + return ( + "Do " + + (expectMove ? "" : "no-op ") + + aPivotMoveMethod + + " in " + + prettyName(aIdOrNameOrAcc) + + ", " + + boundaryToString(aBoundary) + + ", [" + + aTextOffsets + + "]" + ); + }; + + if (expectMove) { + this.eventSeq = [ + new VCChangedChecker( + aDocAcc, + aIdOrNameOrAcc, + aTextOffsets, + aPivotMoveMethod, + aIsFromUserInput === undefined ? true : aIsFromUserInput, + aBoundary + ), + ]; + } else { + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc), + ]; + } +} + +/** + * Move the pivot to the position under the point. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aX [in] screen x coordinate + * @param aY [in] screen y coordinate + * @param aIgnoreNoMatch [in] don't unset position if no object was found at + * point. + * @param aRule [in] traversal rule object + * @param aIdOrNameOrAcc [in] id, accessible or accessible name to expect + * virtual cursor to land on after performing move method. + * false if no move is expected. + */ +function moveVCCoordInvoker( + aDocAcc, + aX, + aY, + aIgnoreNoMatch, + aRule, + aIdOrNameOrAcc +) { + // eslint-disable-next-line mozilla/no-compare-against-boolean-literals + var expectMove = aIdOrNameOrAcc != false; + this.invoke = function virtualCursorChangedInvoker_invoke() { + VCChangedChecker.storePreviousPosAndOffset(aDocAcc.virtualCursor); + var moved = aDocAcc.virtualCursor.moveToPoint( + aRule, + aX, + aY, + aIgnoreNoMatch + ); + SimpleTest.ok( + (expectMove && moved) || (!expectMove && !moved), + "moved pivot" + ); + }; + + this.getID = function setVCPosInvoker_getID() { + return ( + "Do " + (expectMove ? "" : "no-op ") + "moveToPoint " + aIdOrNameOrAcc + ); + }; + + if (expectMove) { + this.eventSeq = [ + new VCChangedChecker(aDocAcc, aIdOrNameOrAcc, null, "moveToPoint", true), + ]; + } else { + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc), + ]; + } +} + +/** + * Change the pivot modalRoot + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aModalRootAcc [in] accessible of the modal root, or null + * @param aExpectedResult [in] error result expected. 0 if expecting success + */ +function setModalRootInvoker(aDocAcc, aModalRootAcc, aExpectedResult) { + this.invoke = function setModalRootInvoker_invoke() { + var errorResult = 0; + try { + aDocAcc.virtualCursor.modalRoot = aModalRootAcc; + } catch (x) { + SimpleTest.ok( + x.result, + "Unexpected exception when changing modal root: " + x + ); + errorResult = x.result; + } + + SimpleTest.is( + errorResult, + aExpectedResult, + "Did not get expected result when changing modalRoot" + ); + }; + + this.getID = function setModalRootInvoker_getID() { + return "Set modalRoot to " + prettyName(aModalRootAcc); + }; + + this.eventSeq = []; + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_VIRTUALCURSOR_CHANGED, aDocAcc), + ]; +} + +/** + * Add invokers to a queue to test a rule and an expected sequence of element ids + * or accessible names for that rule in the given document. + * + * @param aQueue [in] event queue in which to push invoker sequence. + * @param aDocAcc [in] the managing document of the virtual cursor we are + * testing + * @param aRule [in] the traversal rule to use in the invokers + * @param aModalRoot [in] a modal root to use in this traversal sequence + * @param aSequence [in] a sequence of accessible names or element ids to expect + * with the given rule in the given document + */ +function queueTraversalSequence(aQueue, aDocAcc, aRule, aModalRoot, aSequence) { + aDocAcc.virtualCursor.position = null; + + // Add modal root (if any) + aQueue.push(new setModalRootInvoker(aDocAcc, aModalRoot, 0)); + + aQueue.push(new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0])); + + for (let i = 1; i < aSequence.length; i++) { + let invoker = new setVCPosInvoker(aDocAcc, "moveNext", aRule, aSequence[i]); + aQueue.push(invoker); + } + + // No further more matches for given rule, expect no virtual cursor changes. + aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, false)); + + for (let i = aSequence.length - 2; i >= 0; i--) { + let invoker = new setVCPosInvoker( + aDocAcc, + "movePrevious", + aRule, + aSequence[i] + ); + aQueue.push(invoker); + } + + // No previous more matches for given rule, expect no virtual cursor changes. + aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, false)); + + aQueue.push( + new setVCPosInvoker( + aDocAcc, + "moveLast", + aRule, + aSequence[aSequence.length - 1] + ) + ); + + // No further more matches for given rule, expect no virtual cursor changes. + aQueue.push(new setVCPosInvoker(aDocAcc, "moveNext", aRule, false)); + + // set isFromUserInput to false, just to test.. + aQueue.push( + new setVCPosInvoker(aDocAcc, "moveFirst", aRule, aSequence[0], false) + ); + + // No previous more matches for given rule, expect no virtual cursor changes. + aQueue.push(new setVCPosInvoker(aDocAcc, "movePrevious", aRule, false)); + + // Remove modal root (if any). + aQueue.push(new setModalRootInvoker(aDocAcc, null, 0)); +} + +/** + * A checker for removing an accessible while the virtual cursor is on it. + */ +function removeVCPositionChecker(aDocAcc, aHiddenParentAcc) { + this.__proto__ = new invokerChecker(EVENT_REORDER, aHiddenParentAcc); + + this.check = function removeVCPositionChecker_check(aEvent) { + var errorResult = 0; + try { + aDocAcc.virtualCursor.moveNext(ObjectTraversalRule); + } catch (x) { + errorResult = x.result; + } + SimpleTest.is( + errorResult, + NS_ERROR_NOT_IN_TREE, + "Expecting NOT_IN_TREE error when moving pivot from invalid position." + ); + }; +} + +/** + * Put the virtual cursor's position on an object, and then remove it. + * + * @param aDocAcc [in] document that manages the virtual cursor + * @param aPosNode [in] DOM node to hide after virtual cursor's position is + * set to it. + */ +function removeVCPositionInvoker(aDocAcc, aPosNode) { + this.accessible = getAccessible(aPosNode); + this.invoke = function removeVCPositionInvoker_invoke() { + aDocAcc.virtualCursor.position = this.accessible; + aPosNode.remove(); + }; + + this.getID = function removeVCPositionInvoker_getID() { + return "Bring virtual cursor to accessible, and remove its DOM node."; + }; + + this.eventSeq = [ + new removeVCPositionChecker(aDocAcc, this.accessible.parent), + ]; +} + +/** + * A checker for removing the pivot root and then calling moveFirst, and + * checking that an exception is thrown. + */ +function removeVCRootChecker(aPivot) { + this.__proto__ = new invokerChecker(EVENT_REORDER, aPivot.root.parent); + + this.check = function removeVCRootChecker_check(aEvent) { + var errorResult = 0; + try { + aPivot.moveLast(ObjectTraversalRule); + } catch (x) { + errorResult = x.result; + } + SimpleTest.is( + errorResult, + NS_ERROR_NOT_IN_TREE, + "Expecting NOT_IN_TREE error when moving pivot from invalid position." + ); + }; +} + +/** + * Create a pivot, remove its root, and perform an operation where the root is + * needed. + * + * @param aRootNode [in] DOM node of which accessible will be the root of the + * pivot. Should have more than one child. + */ +function removeVCRootInvoker(aRootNode) { + this.pivot = gAccService.createAccessiblePivot(getAccessible(aRootNode)); + this.invoke = function removeVCRootInvoker_invoke() { + this.pivot.position = this.pivot.root.firstChild; + aRootNode.remove(); + }; + + this.getID = function removeVCRootInvoker_getID() { + return "Remove root of pivot from tree."; + }; + + this.eventSeq = [new removeVCRootChecker(this.pivot)]; +} + +/** + * A debug utility for writing proper sequences for queueTraversalSequence. + */ +function dumpTraversalSequence(aPivot, aRule) { + var sequence = []; + if (aPivot.moveFirst(aRule)) { + do { + sequence.push("'" + prettyName(aPivot.position) + "'"); + } while (aPivot.moveNext(aRule)); + } + SimpleTest.info("\n[" + sequence.join(", ") + "]\n"); +} diff --git a/accessible/tests/mochitest/pivot/a11y.ini b/accessible/tests/mochitest/pivot/a11y.ini new file mode 100644 index 0000000000..8add460947 --- /dev/null +++ b/accessible/tests/mochitest/pivot/a11y.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + doc_virtualcursor.html + doc_virtualcursor_text.html + !/accessible/tests/mochitest/*.js + +[test_virtualcursor.html] +[test_virtualcursor_text.html] diff --git a/accessible/tests/mochitest/pivot/doc_virtualcursor.html b/accessible/tests/mochitest/pivot/doc_virtualcursor.html new file mode 100644 index 0000000000..a456f2dfcd --- /dev/null +++ b/accessible/tests/mochitest/pivot/doc_virtualcursor.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<head> + <title>Pivot test document</title> + <meta charset="utf-8" /> +</head> +<body> + <h1 id="heading-1-1">Main Title</h1> + <h2 id="heading-2-1" aria-hidden="true">First Section Title</h2> + <p id="paragraph-1"> + Lorem ipsum <strong>dolor</strong> sit amet. Integer vitae urna + leo, id <a href="#">semper</a> nulla. + </p> + <h2 id="heading-2-2" aria-hidden="undefined">Second Section Title</h2> + <p id="paragraph-2" aria-hidden=""> + Sed accumsan luctus lacus, vitae mollis arcu tristique vulputate.</p> + <p id="paragraph-3" aria-hidden="true"> + <a href="#" id="hidden-link">Maybe</a> it was the other <i>George Michael</i>. + You know, the <a href="#">singer-songwriter</a>. + </p> + <p style="opacity: 0;" id="paragraph-4"> + This is completely transparent + </p> + <iframe + src="data:text/html,<html><body>An <i>embedded</i> document.</body></html>"> + </iframe> + <div id="hide-me">Hide me</div> + <p id="links" aria-hidden="false"> + <a href="http://mozilla.org" title="Link 1 title">Link 1</a> + <a href="http://mozilla.org" title="Link 2 title">Link 2</a> + <a href="http://mozilla.org" title="Link 3 title">Link 3</a> + </p> + <ul> + <li>Hello<span> </span></li> + <li>World</li> + </ul> +</body> +</html> diff --git a/accessible/tests/mochitest/pivot/doc_virtualcursor_text.html b/accessible/tests/mochitest/pivot/doc_virtualcursor_text.html new file mode 100644 index 0000000000..f824de34bd --- /dev/null +++ b/accessible/tests/mochitest/pivot/doc_virtualcursor_text.html @@ -0,0 +1,33 @@ +<!DOCTYPE html> +<html> +<head> + <title>Pivot test document</title> + <meta charset="utf-8" /> +</head> +<body> + <div id="start-block">This is the very beginning.</div> + <p id="paragraph-1"> + This <b>is</b> <a id="p1-link-1" href="#">the</a> test of text. + </p> + <div id="section-1">A <a id="s1-link-1" href="#">multiword link</a> is here. <a id="s1-link-2" href="#">We</a> will traverse</div> + <div id="section-2">into, out, and between the subtrees.</div> + <p id="paragraph-2">Singularity.</p> + <table> + <tr> + <td id="cell-1">Magical</td> + <td id="cell-2">unicorns</td> + </tr> + <tr> + <td id="cell-3">and wizards</td> + <td id="cell-4">really exist.</td> + </tr> + </table> + <div id="section-3">Endless fun!</div> + <p id="paragraph-3">Objects<a id="p3-link-1" href="#">adjacent</a>to <a id="p3-link-2" href="#">each</a><a id="p3-link-3" href="#">other</a> should be separate.</p> + <p id="paragraph-4">Hello <strong>real</strong><a href="#"> world</p> + <a href="#" id="image-desc-link"> + <img src="../moz.png" alt="">Hello + </a> + <div id="end-block">End!</div> +</body> +</html> diff --git a/accessible/tests/mochitest/pivot/test_virtualcursor.html b/accessible/tests/mochitest/pivot/test_virtualcursor.html new file mode 100644 index 0000000000..9f3225fcf6 --- /dev/null +++ b/accessible/tests/mochitest/pivot/test_virtualcursor.html @@ -0,0 +1,119 @@ +<!DOCTYPE html> +<html> +<head> + <title>Tests pivot functionality in virtual cursors</title> + <meta charset="utf-8" /> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"> + </script> + <script src="chrome://mochikit/content/chrome-harness.js"> + </script> + + <script type="application/javascript" src="../common.js"></script> + <script type="application/javascript" src="../browser.js"></script> + <script type="application/javascript" src="../events.js"></script> + <script type="application/javascript" src="../role.js"></script> + <script type="application/javascript" src="../states.js"></script> + <script type="application/javascript" src="../pivot.js"></script> + <script type="application/javascript" src="../layout.js"></script> + + <script type="application/javascript"> + var gBrowserWnd = null; + var gQueue = null; + + function doTest() { + var rootAcc = getAccessible(browserDocument(), [nsIAccessibleDocument]); + ok(rootAcc.virtualCursor, + "root document does not have virtualCursor"); + + var doc = currentTabDocument(); + var docAcc = getAccessible(doc, [nsIAccessibleDocument]); + + // Test that embedded documents have their own virtual cursor. + is(docAcc.childDocumentCount, 1, "Expecting one child document"); + ok(docAcc.getChildDocumentAt(0).virtualCursor, + "child document does not have virtualCursor"); + + gQueue = new eventQueue(); + + gQueue.onFinish = function onFinish() { + closeBrowserWindow(); + }; + + queueTraversalSequence(gQueue, docAcc, HeadersTraversalRule, null, + ["heading-1-1", "heading-2-2"]); + + queueTraversalSequence( + gQueue, docAcc, ObjectTraversalRule, null, + ["Main Title", "Lorem ipsum ", + "dolor", " sit amet. Integer vitae urna leo, id ", + "semper", " nulla. ", "Second Section Title", + "Sed accumsan luctus lacus, vitae mollis arcu tristique vulputate.", + "An ", "embedded", " document.", "Hide me", "Link 1", "Link 2", + "Link 3", "Hello", "World"]); + + // Just a random smoke test to see if our setTextRange works. + gQueue.push( + new setVCRangeInvoker( + docAcc, + getAccessible(doc.getElementById("paragraph-2"), nsIAccessibleText), + [2, 6])); + + gQueue.push(new removeVCPositionInvoker( + docAcc, doc.getElementById("hide-me"))); + + gQueue.push(new removeVCRootInvoker( + doc.getElementById("links"))); + + var [x, y] = getBounds(getAccessible(doc.getElementById("heading-1-1"))); + gQueue.push(new moveVCCoordInvoker(docAcc, x + 1, y + 1, true, + HeadersTraversalRule, "heading-1-1")); + + // Already on the point, so we should not get a move event. + gQueue.push(new moveVCCoordInvoker(docAcc, x + 1, y + 1, true, + HeadersTraversalRule, false)); + + // Attempting a coordinate outside any header, should not move. + gQueue.push(new moveVCCoordInvoker(docAcc, x - 1, y - 1, true, + HeadersTraversalRule, false)); + + // Attempting a coordinate outside any header, should move to null + gQueue.push(new moveVCCoordInvoker(docAcc, x - 1, y - 1, false, + HeadersTraversalRule, null)); + + queueTraversalSequence( + gQueue, docAcc, ObjectTraversalRule, + getAccessible(doc.getElementById("paragraph-1")), + ["Lorem ipsum ", "dolor", " sit amet. Integer vitae urna leo, id ", + "semper", " nulla. "]); + + gQueue.push(new setModalRootInvoker(docAcc, docAcc.parent, + NS_ERROR_INVALID_ARG)); + + gQueue.push(new setVCPosInvoker(docAcc, "moveNext", ObjectTraversalRule, "dolor")); + + gQueue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + /* We open a new browser because we need to test with a top-level content + document. */ + openBrowserWindow( + doTest, + getRootDirectory(window.location.href) + "doc_virtualcursor.html"); + }); + </script> +</head> +<body id="body"> + + <a target="_blank" + title="Introduce virtual cursor/soft focus functionality to a11y API" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=698823">Mozilla Bug 698823</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/pivot/test_virtualcursor_text.html b/accessible/tests/mochitest/pivot/test_virtualcursor_text.html new file mode 100644 index 0000000000..f900afff87 --- /dev/null +++ b/accessible/tests/mochitest/pivot/test_virtualcursor_text.html @@ -0,0 +1,267 @@ +<!DOCTYPE html> +<html> +<head> + <title>Tests pivot functionality in virtual cursors</title> + <meta charset="utf-8" /> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"> + </script> + <script src="chrome://mochikit/content/chrome-harness.js"> + </script> + + <script type="application/javascript" src="../common.js"></script> + <script type="application/javascript" src="../text.js"></script> + <script type="application/javascript" src="../browser.js"></script> + <script type="application/javascript" src="../events.js"></script> + <script type="application/javascript" src="../role.js"></script> + <script type="application/javascript" src="../states.js"></script> + <script type="application/javascript" src="../pivot.js"></script> + <script type="application/javascript" src="../layout.js"></script> + + <script type="application/javascript"> + var gBrowserWnd = null; + var gQueue = null; + + function doTest() { + var doc = currentTabDocument(); + var docAcc = getAccessible(doc, [nsIAccessibleDocument]); + + gQueue = new eventQueue(); + + gQueue.onFinish = function onFinish() { + closeBrowserWindow(); + }; + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("paragraph-1")))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 4], + getAccessible(doc.getElementById("paragraph-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", CHAR_BOUNDARY, [4, 5], + getAccessible(doc.getElementById("paragraph-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", CHAR_BOUNDARY, [3, 4], + getAccessible(doc.getElementById("paragraph-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [5, 7], + getAccessible(doc.getElementById("paragraph-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 3], + getAccessible(doc.getElementById("p1-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [10, 14], + getAccessible(doc.getElementById("paragraph-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 3], + getAccessible(doc.getElementById("p1-link-1"), nsIAccessibleText))); + // set user input to false, and see if it works + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [5, 7], + getAccessible(doc.getElementById("paragraph-1"), nsIAccessibleText)), + false); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("section-1")))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 1], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 9], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText))); + // set user input to false, and see if it works + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [10, 14], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText), + false)); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [4, 6], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [7, 12], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 2], + getAccessible(doc.getElementById("s1-link-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [15, 19], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [20, 28], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 5], + getAccessible(doc.getElementById("section-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [6, 10], + getAccessible(doc.getElementById("section-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 5], + getAccessible(doc.getElementById("section-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [20, 28], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [15, 19], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 2], + getAccessible(doc.getElementById("s1-link-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [7, 12], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [4, 6], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [10, 14], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 9], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 1], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + + gQueue.push(new setVCRangeInvoker(docAcc, getAccessible(doc.getElementById("s1-link-1")), [0, 0])); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", CHAR_BOUNDARY, [1, 2], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", CHAR_BOUNDARY, [0, 1], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", CHAR_BOUNDARY, [1, 2], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", CHAR_BOUNDARY, [0, 1], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", CHAR_BOUNDARY, [1, 2], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [2, 9], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [10, 14], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", CHAR_BOUNDARY, [3, 4], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", CHAR_BOUNDARY, [13, 14], + getAccessible(doc.getElementById("s1-link-1"), nsIAccessibleText))); + + gQueue.push(new setVCRangeInvoker(docAcc, getAccessible(doc.getElementById("section-2")), [0, 0])); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", CHAR_BOUNDARY, [27, 28], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", CHAR_BOUNDARY, [0, 1], + getAccessible(doc.getElementById("section-2"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("paragraph-2")))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 12], + getAccessible(doc.getElementById("paragraph-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 7], + getAccessible(doc.getElementById("cell-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 8], + getAccessible(doc.getElementById("cell-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 3], + getAccessible(doc.getElementById("cell-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [4, 11], + getAccessible(doc.getElementById("cell-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 6], + getAccessible(doc.getElementById("cell-4"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [7, 13], + getAccessible(doc.getElementById("cell-4"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 7], + getAccessible(doc.getElementById("section-3"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("section-3")))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 7], + getAccessible(doc.getElementById("section-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [7, 13], + getAccessible(doc.getElementById("cell-4"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 6], + getAccessible(doc.getElementById("cell-4"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [4, 11], + getAccessible(doc.getElementById("cell-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 3], + getAccessible(doc.getElementById("cell-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 8], + getAccessible(doc.getElementById("cell-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 7], + getAccessible(doc.getElementById("cell-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 12], + getAccessible(doc.getElementById("paragraph-2"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("paragraph-3")))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 7], + getAccessible(doc.getElementById("paragraph-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 8], + getAccessible(doc.getElementById("p3-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [8, 10], + getAccessible(doc.getElementById("paragraph-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 4], + getAccessible(doc.getElementById("p3-link-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 5], + getAccessible(doc.getElementById("p3-link-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [14, 20], + getAccessible(doc.getElementById("paragraph-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 5], + getAccessible(doc.getElementById("p3-link-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 4], + getAccessible(doc.getElementById("p3-link-2"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [8, 10], + getAccessible(doc.getElementById("paragraph-3"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 8], + getAccessible(doc.getElementById("p3-link-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 7], + getAccessible(doc.getElementById("paragraph-3"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("s1-link-2")))); + // Start with the pivot in the middle of the paragraph + gQueue.push(new setVCPosInvoker(docAcc, "moveNext", ObjectTraversalRule, " will traverse")); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [15, 19], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [0, 2], + getAccessible(doc.getElementById("s1-link-2"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("end-block")))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 4], + getAccessible(doc.getElementById("end-block"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, null, false)); + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("start-block")))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 4], + getAccessible(doc.getElementById("start-block"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, null, false)); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, null, false)); + gQueue.push(new setVCRangeInvoker(docAcc, getAccessible(doc.getElementById("start-block")), [0, 0])); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, null, false)); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("paragraph-3")).firstChild)); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [0, 7], + getAccessible(doc.getElementById("paragraph-3"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("s1-link-1")).nextSibling)); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [4, 6], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("paragraph-4")).firstChild.nextSibling)); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", WORD_BOUNDARY, [6, 10], + getAccessible(doc.getElementById("paragraph-4"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("section-1")))); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [20, 28], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("section-1")).lastChild)); + gQueue.push(new setVCTextInvoker(docAcc, "movePreviousByText", WORD_BOUNDARY, [20, 28], + getAccessible(doc.getElementById("section-1"), nsIAccessibleText))); + + gQueue.push(new setVCPosInvoker(docAcc, null, null, + getAccessible(doc.getElementById("image-desc-link")))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", CHAR_BOUNDARY, [0, 1], + getAccessible(doc.getElementById("image-desc-link"), nsIAccessibleText))); + gQueue.push(new setVCTextInvoker(docAcc, "moveNextByText", CHAR_BOUNDARY, [1, 2], + getAccessible(doc.getElementById("image-desc-link"), nsIAccessibleText))); + + gQueue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + addLoadEvent(function() { + /* We open a new browser because we need to test with a top-level content + document. */ + openBrowserWindow( + doTest, + getRootDirectory(window.location.href) + "doc_virtualcursor_text.html"); + }); + </script> +</head> +<body id="body"> + + <a target="_blank" + title="Support Movement By Granularity" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=886076">Mozilla Bug 886076</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/promisified-events.js b/accessible/tests/mochitest/promisified-events.js new file mode 100644 index 0000000000..fcea95bc63 --- /dev/null +++ b/accessible/tests/mochitest/promisified-events.js @@ -0,0 +1,262 @@ +/* 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"; + +// This is loaded by head.js, so has the same globals, hence we import the +// globals from there. +/* import-globals-from common.js */ + +/* exported EVENT_ANNOUNCEMENT, EVENT_REORDER, EVENT_SCROLLING, + EVENT_SCROLLING_END, EVENT_SHOW, EVENT_TEXT_INSERTED, + EVENT_TEXT_REMOVED, EVENT_DOCUMENT_LOAD_COMPLETE, EVENT_HIDE, + EVENT_TEXT_ATTRIBUTE_CHANGED, EVENT_TEXT_CARET_MOVED, + EVENT_DESCRIPTION_CHANGE, EVENT_NAME_CHANGE, EVENT_STATE_CHANGE, + EVENT_VALUE_CHANGE, EVENT_TEXT_VALUE_CHANGE, EVENT_FOCUS, + EVENT_DOCUMENT_RELOAD, EVENT_VIRTUALCURSOR_CHANGED, EVENT_ALERT, + UnexpectedEvents, waitForEvent, waitForEvents, waitForOrderedEvents */ + +const EVENT_ANNOUNCEMENT = nsIAccessibleEvent.EVENT_ANNOUNCEMENT; +const EVENT_DOCUMENT_LOAD_COMPLETE = + nsIAccessibleEvent.EVENT_DOCUMENT_LOAD_COMPLETE; +const EVENT_HIDE = nsIAccessibleEvent.EVENT_HIDE; +const EVENT_REORDER = nsIAccessibleEvent.EVENT_REORDER; +const EVENT_SCROLLING = nsIAccessibleEvent.EVENT_SCROLLING; +const EVENT_SCROLLING_END = nsIAccessibleEvent.EVENT_SCROLLING_END; +const EVENT_SHOW = nsIAccessibleEvent.EVENT_SHOW; +const EVENT_STATE_CHANGE = nsIAccessibleEvent.EVENT_STATE_CHANGE; +const EVENT_TEXT_ATTRIBUTE_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_ATTRIBUTE_CHANGED; +const EVENT_TEXT_CARET_MOVED = nsIAccessibleEvent.EVENT_TEXT_CARET_MOVED; +const EVENT_TEXT_INSERTED = nsIAccessibleEvent.EVENT_TEXT_INSERTED; +const EVENT_TEXT_REMOVED = nsIAccessibleEvent.EVENT_TEXT_REMOVED; +const EVENT_DESCRIPTION_CHANGE = nsIAccessibleEvent.EVENT_DESCRIPTION_CHANGE; +const EVENT_NAME_CHANGE = nsIAccessibleEvent.EVENT_NAME_CHANGE; +const EVENT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_VALUE_CHANGE; +const EVENT_TEXT_VALUE_CHANGE = nsIAccessibleEvent.EVENT_TEXT_VALUE_CHANGE; +const EVENT_FOCUS = nsIAccessibleEvent.EVENT_FOCUS; +const EVENT_DOCUMENT_RELOAD = nsIAccessibleEvent.EVENT_DOCUMENT_RELOAD; +const EVENT_VIRTUALCURSOR_CHANGED = + nsIAccessibleEvent.EVENT_VIRTUALCURSOR_CHANGED; +const EVENT_ALERT = nsIAccessibleEvent.EVENT_ALERT; +const EVENT_TEXT_SELECTION_CHANGED = + nsIAccessibleEvent.EVENT_TEXT_SELECTION_CHANGED; +const EVENT_LIVE_REGION_ADDED = nsIAccessibleEvent.EVENT_LIVE_REGION_ADDED; +const EVENT_LIVE_REGION_REMOVED = nsIAccessibleEvent.EVENT_LIVE_REGION_REMOVED; + +const EventsLogger = { + enabled: false, + + log(msg) { + if (this.enabled) { + info(msg); + } + }, +}; + +/** + * Describe an event in string format. + * @param {nsIAccessibleEvent} event event to strigify + */ +function eventToString(event) { + let type = eventTypeToString(event.eventType); + let info = `Event type: ${type}`; + + if (event instanceof nsIAccessibleStateChangeEvent) { + let stateStr = statesToString( + event.isExtraState ? 0 : event.state, + event.isExtraState ? event.state : 0 + ); + info += `, state: ${stateStr}, is enabled: ${event.isEnabled}`; + } else if (event instanceof nsIAccessibleTextChangeEvent) { + let tcType = event.isInserted ? "inserted" : "removed"; + info += `, start: ${event.start}, length: ${event.length}, ${tcType} text: ${event.modifiedText}`; + } + + info += `. Target: ${prettyName(event.accessible)}`; + return info; +} + +function matchEvent(event, matchCriteria) { + if (!matchCriteria) { + return true; + } + + let acc = event.accessible; + switch (typeof matchCriteria) { + case "string": + let id = getAccessibleDOMNodeID(acc); + if (id === matchCriteria) { + EventsLogger.log(`Event matches DOMNode id: ${id}`); + return true; + } + break; + case "function": + if (matchCriteria(event)) { + EventsLogger.log( + `Lambda function matches event: ${eventToString(event)}` + ); + return true; + } + break; + default: + if (matchCriteria instanceof nsIAccessible) { + if (acc === matchCriteria) { + EventsLogger.log(`Event matches accessible: ${prettyName(acc)}`); + return true; + } + } else if (event.DOMNode == matchCriteria) { + EventsLogger.log( + `Event matches DOM node: ${prettyName(event.DOMNode)}` + ); + return true; + } + } + + return false; +} + +/** + * A helper function that returns a promise that resolves when an accessible + * event of the given type with the given target (defined by its id or + * accessible) is observed. + * @param {Number} eventType expected accessible event + * type + * @param {String|nsIAccessible|Function} matchCriteria expected content + * element id + * for the event + * @param {String} message Message to prepend to logging. + * @return {Promise} promise that resolves to an + * event + */ +function waitForEvent(eventType, matchCriteria, message) { + return new Promise(resolve => { + let eventObserver = { + observe(subject, topic, data) { + if (topic !== "accessible-event") { + return; + } + + let event = subject.QueryInterface(nsIAccessibleEvent); + if (EventsLogger.enabled) { + // Avoid calling eventToString if the EventsLogger isn't enabled in order + // to avoid an intermittent crash (bug 1307645). + EventsLogger.log(eventToString(event)); + } + + // If event type does not match expected type, skip the event. + if (event.eventType !== eventType) { + return; + } + + if (matchEvent(event, matchCriteria)) { + EventsLogger.log( + `Correct event type: ${eventTypeToString(eventType)}` + ); + Services.obs.removeObserver(this, "accessible-event"); + ok( + true, + `${message ? message + ": " : ""}Recieved ${eventTypeToString( + eventType + )} event` + ); + resolve(event); + } + }, + }; + Services.obs.addObserver(eventObserver, "accessible-event"); + }); +} + +class UnexpectedEvents { + constructor(unexpected) { + if (unexpected.length) { + this.unexpected = unexpected; + Services.obs.addObserver(this, "accessible-event"); + } + } + + observe(subject, topic, data) { + if (topic !== "accessible-event") { + return; + } + + let event = subject.QueryInterface(nsIAccessibleEvent); + + let unexpectedEvent = this.unexpected.find( + ([etype, criteria]) => + etype === event.eventType && matchEvent(event, criteria) + ); + + if (unexpectedEvent) { + ok(false, `Got unexpected event: ${eventToString(event)}`); + } + } + + stop() { + if (this.unexpected) { + Services.obs.removeObserver(this, "accessible-event"); + } + } +} + +/** + * A helper function that waits for a sequence of accessible events in + * specified order. + * @param {Array} events a list of events to wait (same format as + * waitForEvent arguments) + * @param {String} message Message to prepend to logging. + * @param {Boolean} ordered Events need to be recieved in given order. + */ +async function waitForEvents(events, message, ordered = false) { + let expected = events.expected || events; + let unexpected = events.unexpected || []; + // Next expected event index. + let currentIdx = 0; + + let unexpectedListener = new UnexpectedEvents(unexpected); + + let results = await Promise.all( + expected.map((evt, idx) => { + const [eventType, matchCriteria] = evt; + return waitForEvent(eventType, matchCriteria, message).then(result => { + return [result, idx == currentIdx++]; + }); + }) + ); + + unexpectedListener.stop(); + + if (ordered) { + ok( + results.every(([, isOrdered]) => isOrdered), + `${message ? message + ": " : ""}Correct event order` + ); + } + + return results.map(([event]) => event); +} + +function waitForOrderedEvents(events, message) { + return waitForEvents(events, message, true); +} + +//////////////////////////////////////////////////////////////////////////////// +// Utility functions ported from events.js. + +/** + * This function selects all text in the passed-in element if it has an editor, + * before setting focus to it. This simulates behavio with the keyboard when + * tabbing to the element. This does explicitly what synthFocus did implicitly. + * This should be called only if you really want this behavior. + * @param {string} id The element ID to focus + */ +function selectAllTextAndFocus(id) { + const elem = getNode(id); + if (elem.editor) { + elem.selectionStart = elem.selectionEnd = elem.value.length; + } + + elem.focus(); +} diff --git a/accessible/tests/mochitest/relations.js b/accessible/tests/mochitest/relations.js new file mode 100644 index 0000000000..fdb4ed428f --- /dev/null +++ b/accessible/tests/mochitest/relations.js @@ -0,0 +1,203 @@ +/* import-globals-from common.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Constants + +var RELATION_CONTROLLED_BY = nsIAccessibleRelation.RELATION_CONTROLLED_BY; +var RELATION_CONTROLLER_FOR = nsIAccessibleRelation.RELATION_CONTROLLER_FOR; +var RELATION_DEFAULT_BUTTON = nsIAccessibleRelation.RELATION_DEFAULT_BUTTON; +var RELATION_DESCRIBED_BY = nsIAccessibleRelation.RELATION_DESCRIBED_BY; +var RELATION_DESCRIPTION_FOR = nsIAccessibleRelation.RELATION_DESCRIPTION_FOR; +var RELATION_EMBEDDED_BY = nsIAccessibleRelation.RELATION_EMBEDDED_BY; +var RELATION_EMBEDS = nsIAccessibleRelation.RELATION_EMBEDS; +var RELATION_FLOWS_FROM = nsIAccessibleRelation.RELATION_FLOWS_FROM; +var RELATION_FLOWS_TO = nsIAccessibleRelation.RELATION_FLOWS_TO; +var RELATION_LABEL_FOR = nsIAccessibleRelation.RELATION_LABEL_FOR; +var RELATION_LABELLED_BY = nsIAccessibleRelation.RELATION_LABELLED_BY; +var RELATION_MEMBER_OF = nsIAccessibleRelation.RELATION_MEMBER_OF; +var RELATION_NODE_CHILD_OF = nsIAccessibleRelation.RELATION_NODE_CHILD_OF; +var RELATION_NODE_PARENT_OF = nsIAccessibleRelation.RELATION_NODE_PARENT_OF; +var RELATION_PARENT_WINDOW_OF = nsIAccessibleRelation.RELATION_PARENT_WINDOW_OF; +var RELATION_POPUP_FOR = nsIAccessibleRelation.RELATION_POPUP_FOR; +var RELATION_SUBWINDOW_OF = nsIAccessibleRelation.RELATION_SUBWINDOW_OF; +var RELATION_CONTAINING_DOCUMENT = + nsIAccessibleRelation.RELATION_CONTAINING_DOCUMENT; +var RELATION_CONTAINING_TAB_PANE = + nsIAccessibleRelation.RELATION_CONTAINING_TAB_PANE; +var RELATION_CONTAINING_APPLICATION = + nsIAccessibleRelation.RELATION_CONTAINING_APPLICATION; +const RELATION_DETAILS = nsIAccessibleRelation.RELATION_DETAILS; +const RELATION_DETAILS_FOR = nsIAccessibleRelation.RELATION_DETAILS_FOR; +const RELATION_ERRORMSG = nsIAccessibleRelation.RELATION_ERRORMSG; +const RELATION_ERRORMSG_FOR = nsIAccessibleRelation.RELATION_ERRORMSG_FOR; + +// ////////////////////////////////////////////////////////////////////////////// +// General + +/** + * Test the accessible relation. + * + * @param aIdentifier [in] identifier to get an accessible, may be ID + * attribute or DOM element or accessible object + * @param aRelType [in] relation type (see constants above) + * @param aRelatedIdentifiers [in] identifier or array of identifiers of + * expected related accessibles + */ +function testRelation(aIdentifier, aRelType, aRelatedIdentifiers) { + var relation = getRelationByType(aIdentifier, aRelType); + + var relDescr = getRelationErrorMsg(aIdentifier, aRelType); + var relDescrStart = getRelationErrorMsg(aIdentifier, aRelType, true); + + if (!relation || !relation.targetsCount) { + if (!aRelatedIdentifiers) { + ok(true, "No" + relDescr); + return; + } + + var msg = + relDescrStart + + "has no expected targets: '" + + prettyName(aRelatedIdentifiers) + + "'"; + + ok(false, msg); + return; + } else if (!aRelatedIdentifiers) { + ok(false, "There are unexpected targets of " + relDescr); + return; + } + + var relatedIds = + aRelatedIdentifiers instanceof Array + ? aRelatedIdentifiers + : [aRelatedIdentifiers]; + + var targets = []; + for (let idx = 0; idx < relatedIds.length; idx++) { + targets.push(getAccessible(relatedIds[idx])); + } + + if (targets.length != relatedIds.length) { + return; + } + + var actualTargets = relation.getTargets(); + + // Check if all given related accessibles are targets of obtained relation. + for (let idx = 0; idx < targets.length; idx++) { + var isFound = false; + for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) { + if (targets[idx] == relatedAcc) { + isFound = true; + break; + } + } + + ok(isFound, prettyName(relatedIds[idx]) + " is not a target of" + relDescr); + } + + // Check if all obtained targets are given related accessibles. + for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) { + let idx; + // eslint-disable-next-line no-empty + for (idx = 0; idx < targets.length && relatedAcc != targets[idx]; idx++) {} + + if (idx == targets.length) { + ok( + false, + "There is unexpected target" + prettyName(relatedAcc) + "of" + relDescr + ); + } + } +} + +/** + * Test that the given accessible relations don't exist. + * + * @param aIdentifier [in] identifier to get an accessible, may be ID + * attribute or DOM element or accessible object + * @param aRelType [in] relation type (see constants above) + * @param aUnrelatedIdentifiers [in] identifier or array of identifiers of + * accessibles that shouldn't exist for this + * relation. + */ +function testAbsentRelation(aIdentifier, aRelType, aUnrelatedIdentifiers) { + var relation = getRelationByType(aIdentifier, aRelType); + + var relDescr = getRelationErrorMsg(aIdentifier, aRelType); + + if (!aUnrelatedIdentifiers) { + ok(false, "No identifiers given for unrelated accessibles."); + return; + } + + if (!relation || !relation.targetsCount) { + ok(true, "No relations exist."); + return; + } + + var relatedIds = + aUnrelatedIdentifiers instanceof Array + ? aUnrelatedIdentifiers + : [aUnrelatedIdentifiers]; + + var targets = []; + for (let idx = 0; idx < relatedIds.length; idx++) { + targets.push(getAccessible(relatedIds[idx])); + } + + if (targets.length != relatedIds.length) { + return; + } + + var actualTargets = relation.getTargets(); + + // Any found targets that match given accessibles should be called out. + for (let idx = 0; idx < targets.length; idx++) { + var notFound = true; + for (let relatedAcc of actualTargets.enumerate(Ci.nsIAccessible)) { + if (targets[idx] == relatedAcc) { + notFound = false; + break; + } + } + + ok(notFound, prettyName(relatedIds[idx]) + " is a target of " + relDescr); + } +} + +/** + * Return related accessible for the given relation type. + * + * @param aIdentifier [in] identifier to get an accessible, may be ID attribute + * or DOM element or accessible object + * @param aRelType [in] relation type (see constants above) + */ +function getRelationByType(aIdentifier, aRelType) { + var acc = getAccessible(aIdentifier); + if (!acc) { + return null; + } + + var relation = null; + try { + relation = acc.getRelationByType(aRelType); + } catch (e) { + ok(false, "Can't get" + getRelationErrorMsg(aIdentifier, aRelType)); + } + + return relation; +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private implementation details + +function getRelationErrorMsg(aIdentifier, aRelType, aIsStartSentence) { + var relStr = relationTypeToString(aRelType); + var msg = aIsStartSentence ? "Relation of '" : " relation of '"; + msg += relStr + "' type for '" + prettyName(aIdentifier) + "'"; + msg += aIsStartSentence ? " " : "."; + + return msg; +} diff --git a/accessible/tests/mochitest/relations/a11y.ini b/accessible/tests/mochitest/relations/a11y.ini new file mode 100644 index 0000000000..89ffaffdce --- /dev/null +++ b/accessible/tests/mochitest/relations/a11y.ini @@ -0,0 +1,14 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_embeds.xhtml] +skip-if = os == 'linux' && !debug # bug 1411145 +[test_general.html] +[test_general.xhtml] +[test_groupInfoUpdate.html] +[test_tabbrowser.xhtml] +[test_tree.xhtml] +[test_ui_modalprompt.html] +[test_shadowdom.html] +[test_update.html] diff --git a/accessible/tests/mochitest/relations/test_embeds.xhtml b/accessible/tests/mochitest/relations/test_embeds.xhtml new file mode 100644 index 0000000000..4c2fc7f9b9 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_embeds.xhtml @@ -0,0 +1,128 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Embeds relation tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function loadURI(aURI) + { + this.invoke = function loadURI_invoke() + { + tabBrowser().loadURI(aURI, { + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + } + + this.eventSeq = [ + new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument) + ]; + + this.finalCheck = function loadURI_finalCheck() + { + testRelation(browserDocument(), RELATION_EMBEDS, + getAccessible(currentTabDocument())); + } + + this.getID = function loadURI_getID() + { + return "load uri " + aURI; + } + } + + function loadOneTab(aURI) + { + this.invoke = function loadOneTab_invoke() + { + tabBrowser().loadOneTab(aURI, { + referrerURI: null, + charset: null, + postData: null, + inBackground: false, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + } + + this.eventSeq = [ + new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, currentTabDocument) + ]; + + this.finalCheck = function loadURI_finalCheck() + { + testRelation(browserDocument(), RELATION_EMBEDS, + getAccessible(currentTabDocument())); + } + + this.getID = function loadOneTab_getID() + { + return "load uri '" + aURI + "' in new tab"; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Testing + + //gA11yEventDumpToConsole = true; // debug + + var gQueue = null; + function doTests() + { + testRelation(browserDocument(), RELATION_EMBEDS, + getAccessible(currentTabDocument())); + + enableLogging("docload"); + gQueue = new eventQueue(); + + gQueue.push(new loadURI("about:robots")); + gQueue.push(new loadOneTab("about:mozilla")); + + gQueue.onFinish = function() + { + disableLogging(); + closeBrowserWindow(); + } + gQueue.invoke(); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTests, "about:license"); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=707654" + title="Embeds relation on root accessible can return not content document"> + Mozilla Bug 707654 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + </vbox> +</window> diff --git a/accessible/tests/mochitest/relations/test_general.html b/accessible/tests/mochitest/relations/test_general.html new file mode 100644 index 0000000000..e003d3f8e7 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_general.html @@ -0,0 +1,408 @@ +<html> + +<head> + <title>nsIAccessible::getAccessibleRelated() tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // html:label@for + testRelation("label1_1", RELATION_LABEL_FOR, "control1_1"); + testRelation("control1_1", RELATION_LABELLED_BY, "label1_1"); + + // html:label@for, multiple + testRelation("label1_2", RELATION_LABEL_FOR, "control1_2"); + testRelation("label1_3", RELATION_LABEL_FOR, "control1_2"); + testRelation("control1_2", RELATION_LABELLED_BY, + [ "label1_2", "label1_3" ]); + + // ancestor html:label (implicit association) + testRelation("label1_4", RELATION_LABEL_FOR, "control1_4"); + testRelation("control1_4", RELATION_LABELLED_BY, "label1_4"); + testRelation("control1_4_option1", RELATION_LABELLED_BY, null); + testRelation("label1_5", RELATION_LABEL_FOR, "control1_5"); + testRelation("control1_5", RELATION_LABELLED_BY, "label1_5"); + testRelation("label1_6", RELATION_LABEL_FOR, "control1_6"); + testRelation("control1_6", RELATION_LABELLED_BY, "label1_6"); + testRelation("label1_7", RELATION_LABEL_FOR, "control1_7"); + testRelation("control1_7", RELATION_LABELLED_BY, "label1_7"); + testRelation("label1_8", RELATION_LABEL_FOR, "control1_8"); + testRelation("control1_8", RELATION_LABELLED_BY, "label1_8"); + testRelation("label1_9", RELATION_LABEL_FOR, "control1_9"); + testRelation("control1_9", RELATION_LABELLED_BY, "label1_9"); + testRelation("label1_10", RELATION_LABEL_FOR, "control1_10"); + testRelation("control1_10", RELATION_LABELLED_BY, "label1_10"); + testRelation("label1_11", RELATION_LABEL_FOR, "control1_11"); + testRelation("control1_11", RELATION_LABELLED_BY, "label1_11"); + testRelation("label1_12", RELATION_LABEL_FOR, "control1_12"); + testRelation("control1_12", RELATION_LABELLED_BY, "label1_12"); + + testRelation("label1_13", RELATION_LABEL_FOR, null); + testRelation("control1_13", RELATION_LABELLED_BY, null); + testRelation("control1_14", RELATION_LABELLED_BY, "label1_14"); + + // aria-labelledby + testRelation("label2", RELATION_LABEL_FOR, "checkbox2"); + testRelation("checkbox2", RELATION_LABELLED_BY, "label2"); + + // aria-labelledby, multiple relations + testRelation("label3", RELATION_LABEL_FOR, "checkbox3"); + testRelation("label4", RELATION_LABEL_FOR, "checkbox3"); + testRelation("checkbox3", RELATION_LABELLED_BY, ["label3", "label4"]); + + // aria-describedby + testRelation("descr1", RELATION_DESCRIPTION_FOR, "checkbox4"); + testRelation("checkbox4", RELATION_DESCRIBED_BY, "descr1"); + + // aria-describedby, multiple relations + testRelation("descr2", RELATION_DESCRIPTION_FOR, "checkbox5"); + testRelation("descr3", RELATION_DESCRIPTION_FOR, "checkbox5"); + testRelation("checkbox5", RELATION_DESCRIBED_BY, ["descr2", "descr3"]); + + // aria_owns, multiple relations + testRelation("treeitem1", RELATION_NODE_CHILD_OF, "tree"); + testRelation("treeitem2", RELATION_NODE_CHILD_OF, "tree"); + + // 'node child of' relation for outlineitem role + testRelation("treeitem3", RELATION_NODE_CHILD_OF, "tree"); + testRelation("treeitem4", RELATION_NODE_CHILD_OF, "tree"); + testRelation("treeitem5", RELATION_NODE_CHILD_OF, "treeitem4"); + testRelation("treeitem6", RELATION_NODE_CHILD_OF, "tree"); + testRelation("treeitem7", RELATION_NODE_CHILD_OF, "treeitem6"); + testRelation("tree2_ti1", RELATION_NODE_CHILD_OF, "tree2"); + testRelation("tree2_ti1a", RELATION_NODE_CHILD_OF, "tree2_ti1"); + testRelation("tree2_ti1b", RELATION_NODE_CHILD_OF, "tree2_ti1"); + + // 'node child of' relation for row role in grid. + // Relation for row associated using aria-level should exist. + testRelation("simplegrid-row3", RELATION_NODE_CHILD_OF, + "simplegrid-row2"); + // Relations for hierarchical children elements shouldn't exist. + testAbsentRelation("simplegrid-row1", RELATION_NODE_CHILD_OF, + "simplegrid"); + testAbsentRelation("simplegrid-row2", RELATION_NODE_CHILD_OF, + "simplegrid"); + + // 'node child of' relation for row role of treegrid + testRelation("treegridrow1", RELATION_NODE_CHILD_OF, "treegrid"); + testRelation("treegridrow2", RELATION_NODE_CHILD_OF, "treegrid"); + testRelation("treegridrow3", RELATION_NODE_CHILD_OF, "treegridrow2"); + + // 'node child of' relation for lists organized by groups + testRelation("listitem1", RELATION_NODE_CHILD_OF, "list"); + testRelation("listitem1.1", RELATION_NODE_CHILD_OF, "listitem1"); + testRelation("listitem1.2", RELATION_NODE_CHILD_OF, "listitem1"); + + // 'node child of' relation for the document having window, returns + // direct accessible parent (fixed in bug 419770). + var iframeElmObj = {}; + var iframeAcc = getAccessible("iframe", null, iframeElmObj); + var iframeDoc = iframeElmObj.value.contentDocument; + var iframeDocAcc = getAccessible(iframeDoc); + testRelation(iframeDocAcc, RELATION_NODE_CHILD_OF, iframeAcc); + + // 'node parent of' relation on ARIA tree and treegrid. + testRelation("tree", RELATION_NODE_PARENT_OF, + ["treeitem1", "treeitem2", // aria-owns + "treeitem3", "treeitem4", "treeitem6"]); // children + testRelation("treeitem4", RELATION_NODE_PARENT_OF, + "treeitem5"); // aria-level + testRelation("treeitem6", RELATION_NODE_PARENT_OF, + "treeitem7"); // // group role + testRelation("tree2", RELATION_NODE_PARENT_OF, "tree2_ti1"); // group role + testRelation("tree2_ti1", RELATION_NODE_PARENT_OF, + ["tree2_ti1a", "tree2_ti1b"]); // group role + + testRelation("treegridrow2", RELATION_NODE_PARENT_OF, "treegridrow3"); + testRelation("treegrid", RELATION_NODE_PARENT_OF, + ["treegridrow1", "treegridrow2"]); + + // 'node parent of' relation on ARIA grid. + // 'node parent of' relation on ARIA grid's row. + // Should only have relation to child through aria-level. + testRelation("simplegrid-row2", RELATION_NODE_PARENT_OF, + "simplegrid-row3"); + + // 'node parent of' relation on ARIA list structured by groups + testRelation("list", RELATION_NODE_PARENT_OF, + "listitem1"); + testRelation("listitem1", RELATION_NODE_PARENT_OF, + [ "listitem1.1", "listitem1.2" ]); + + // aria-atomic + testRelation(getNode("atomic").firstChild, RELATION_MEMBER_OF, "atomic"); + + // aria-controls + getAccessible("tab"); + todo(false, + "Getting an accessible tab, otherwise relations for tabpanel aren't cached. Bug 606924 will fix that."); + testRelation("tabpanel", RELATION_CONTROLLED_BY, "tab"); + testRelation("tab", RELATION_CONTROLLER_FOR, "tabpanel"); + + // aria-controls, multiple relations + testRelation("lr1", RELATION_CONTROLLED_BY, "button"); + testRelation("lr2", RELATION_CONTROLLED_BY, "button"); + testRelation("button", RELATION_CONTROLLER_FOR, ["lr1", "lr2"]); + + // aria-flowto + testRelation("flowto", RELATION_FLOWS_TO, "flowfrom"); + testRelation("flowfrom", RELATION_FLOWS_FROM, "flowto"); + + // aria-flowto, multiple relations + testRelation("flowto1", RELATION_FLOWS_TO, ["flowfrom1", "flowfrom2"]); + testRelation("flowfrom1", RELATION_FLOWS_FROM, "flowto1"); + testRelation("flowfrom2", RELATION_FLOWS_FROM, "flowto1"); + + // 'default button' relation + testRelation("input", RELATION_DEFAULT_BUTTON, "submit"); + + // output 'for' relations + testRelation("output", RELATION_CONTROLLED_BY, ["input", "input2"]); + testRelation("output2", RELATION_CONTROLLED_BY, ["input", "input2"]); + testRelation("input", RELATION_CONTROLLER_FOR, ["output", "output2"]); + testRelation("input2", RELATION_CONTROLLER_FOR, ["output", "output2"]); + + // 'described by'/'description for' relation for html:table and + // html:caption + testRelation("caption", RELATION_LABEL_FOR, "table"); + testRelation("table", RELATION_LABELLED_BY, "caption"); + + // 'labelled by'/'label for' relation for html:fieldset and + // html:legend + testRelation("legend", RELATION_LABEL_FOR, "fieldset"); + testRelation("fieldset", RELATION_LABELLED_BY, "legend"); + + // containing relations + testRelation("control1_1", RELATION_CONTAINING_DOCUMENT, document); + testRelation("control1_1", RELATION_CONTAINING_TAB_PANE, getTabDocAccessible("control1_1")); + testRelation("control1_1", RELATION_CONTAINING_APPLICATION, getApplicationAccessible()); + + // details + testRelation("has_details", RELATION_DETAILS, "details"); + testRelation("details", RELATION_DETAILS_FOR, "has_details"); + testRelation("has_multiple_details", RELATION_DETAILS, ["details2", "details3"]); + testRelation("details2", RELATION_DETAILS_FOR, "has_multiple_details"); + testRelation("details3", RELATION_DETAILS_FOR, "has_multiple_details"); + + // error + testRelation("has_error", RELATION_ERRORMSG, "error"); + testRelation("error", RELATION_ERRORMSG_FOR, "has_error"); + + // finish test + SimpleTest.finish(); + } + + disableLogging(); // from test_embeds.xhtml + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=475298" + title="mochitests for accessible relations"> + Bug 475298 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=527461" + title="Implement RELATION_NODE_PARENT_OF"> + Bug 527461 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=558036" + title="make HTML <output> accessible"> + Bug 558036 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=682790" + title="Ignore implicit label association when it's associated explicitly"> + Bug 682790 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=687393" + title="HTML select options gets relation from containing label"> + Bug 687393 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=864224" + title="Support nested ARIA listitems structured by role='group'"> + Bug 864224 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <label id="label1_1" for="control1_1">label</label> + <input id="control1_1"> + + <label id="label1_2" for="control1_2">label</label> + <label id="label1_3" for="control1_2">label</label> + <input id="control1_2"> + + <label id="label1_4">Label + <select id="control1_4"> + <option id="control1_4_option1">option</option> + </select> + </label> + <label id="label1_5">Label + <button id="control1_5">button</button> + </label> + <label id="label1_6">Label + <input id="control1_6"> + </label> + <label id="label1_7">Label + <input id="control1_7" type="checkbox"> + </label> + <label id="label1_8">Label + <input id="control1_8" type="radio"> + </label> + <label id="label1_9">Label + <input id="control1_9" type="button" value="button"> + </label> + <label id="label1_10">Label + <input id="control1_10" type="submit"> + </label> + <label id="label1_11">Label + <input id="control1_11" type="image"> + </label> + <label id="label1_12">Label + <progress id="control1_12"></progress> + </label> + + <label id="label1_13" for="">Label + <input id="control1_13"> + </label> + <label id="label1_14" for="control1_14">Label + <input id="control1_14"> + </label> + + <span id="label2">label</span> + <span role="checkbox" id="checkbox2" aria-labelledby="label2"></span> + + <span id="label3">label1</span> + <span id="label4">label2</span> + <span role="checkbox" id="checkbox3" aria-labelledby="label3 label4"></span> + + <span id="descr1">description</span> + <span role="checkbox" id="checkbox4" aria-describedby="descr1"></span> + + <span id="descr2">description1</span> + <span id="descr3">description2</span> + <span role="checkbox" id="checkbox5" aria-describedby="descr2 descr3"></span> + + <div role="treeitem" id="treeitem1">Yellow</div> + <div role="treeitem" id="treeitem2">Orange</div> + <div id="tree" role="tree" aria-owns="treeitem1 treeitem2"> + <div role="treeitem" id="treeitem3">Blue</div> + <div role="treeitem" id="treeitem4" aria-level="1">Green</div> + <div role="treeitem" id="treeitem5" aria-level="2">Light green</div> + <div role="treeitem" id="treeitem6" aria-level="1">Green2</div> + <div role="group"> + <div role="treeitem" id="treeitem7">Super light green</div> + </div> + </div> + + <div role="grid" id="simplegrid"> + <div role="row" id="simplegrid-row1" aria-level="1"> + <div role="gridcell">cell 1,1</div> + <div role="gridcell">cell 1,2</div> + </div> + <div role="row" id="simplegrid-row2" aria-level="1"> + <div role="gridcell">cell 2,1</div> + <div role="gridcell">cell 2,2</div> + </div> + <div role="row" id="simplegrid-row3" aria-level="2"> + <div role="gridcell">cell 3,1</div> + <div role="gridcell">cell 3,2</div> + </div> + </div> + + <ul role="tree" id="tree2"> + <li role="treeitem" id="tree2_ti1">Item 1 + <ul role="group"> + <li role="treeitem" id="tree2_ti1a">Item 1A</li> + <li role="treeitem" id="tree2_ti1b">Item 1B</li> + </ul> + </li> + </ul> + + <div role="treegrid" id="treegrid"> + <div role="row" id="treegridrow1"> + <span role="gridcell">cell1</span><span role="gridcell">cell2</span> + </div> + <div role="row" id="treegridrow2" aria-level="1"> + <span role="gridcell">cell3</span><span role="gridcell">cell4</span> + </div> + <div role="row" id="treegridrow3" aria-level="2"> + <span role="gridcell">cell5</span><span role="gridcell">cell6</span> + </div> + </div> + + <div role="list" id="list"> + <div role="listitem" id="listitem1">Item 1 + <div role="group"> + <div role="listitem" id="listitem1.1">Item 1A</div> + <div role="listitem" id="listitem1.2">Item 1B</div> + </div> + </div> + </div> + + <iframe id="iframe"></iframe> + + <div id="tablist" role="tablist"> + <div id="tab" role="tab" aria-controls="tabpanel">tab</div> + </div> + <div id="tabpanel" role="tabpanel">tabpanel</div> + + <div id="lr1" aria-live="assertive">1</div> + <div id="lr2" aria-live="assertive">a</div> + <input type="button" id="button" aria-controls="lr1 lr2" + onclick="getNode('lr1').textContent += '1'; getNode('lr2').textContent += 'a';"/> + + <div id="atomic" aria-atomic="true">live region</div> + + <span id="flowto" aria-flowto="flowfrom">flow to</span> + <span id="flowfrom">flow from</span> + + <span id="flowto1" aria-flowto="flowfrom1 flowfrom2">flow to</span> + <span id="flowfrom1">flow from</span> + <span id="flowfrom2">flow from</span> + + <form id="form"> + <input id="input" /> + <input id="input2" /> + <input type="submit" id="submit" /> + <output id="output" style="display:block" for="input input2"></output> + <output id="output2" for="input input2"></output> + </form> + + <table id="table"> + <caption id="caption">tabple caption</caption> + <tr> + <td>cell1</td><td>cell2</td> + </tr> + </table> + + <fieldset id="fieldset"> + <legend id="legend">legend</legend> + <input /> + </fieldset> + + <input id="has_details" aria-details="details"><section id="details"></section> + <input id="has_multiple_details" aria-details="details2 details3"><section id="details2"></section><section id="details3"></section> + <input id="has_error" aria-errormessage="error"><section id="error"></section> +</body> +</html> diff --git a/accessible/tests/mochitest/relations/test_general.xhtml b/accessible/tests/mochitest/relations/test_general.xhtml new file mode 100644 index 0000000000..bc3b328fd9 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_general.xhtml @@ -0,0 +1,237 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="nsIAccessible::getAccessibleRelated() tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../relations.js" /> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + function doTest() + { + // xul:label@control + testRelation("label1", RELATION_LABEL_FOR, "checkbox1"); + testRelation("checkbox1", RELATION_LABELLED_BY, "label1"); + + // xul:label@control, multiple + testRelation("label1_1", RELATION_LABEL_FOR, "checkbox1_1"); + testRelation("label1_2", RELATION_LABEL_FOR, "checkbox1_1"); + testRelation("checkbox1_1", RELATION_LABELLED_BY, + [ "label1_1", "label1_2" ]); + + // aria-labelledby + testRelation("label2", RELATION_LABEL_FOR, "checkbox2"); + testRelation("checkbox2", RELATION_LABELLED_BY, "label2"); + + // aria-labelledby, multiple relations + testRelation("label3", RELATION_LABEL_FOR, "checkbox3"); + testRelation("label4", RELATION_LABEL_FOR, "checkbox3"); + testRelation("checkbox3", RELATION_LABELLED_BY, ["label3", "label4"]); + + // xul:label@control referring to HTML element + testRelation("label_input", RELATION_LABEL_FOR, "input"); + testRelation("input", RELATION_LABELLED_BY, "label_input"); + + // aria-describedby + testRelation("descr1", RELATION_DESCRIPTION_FOR, "checkbox4"); + testRelation("checkbox4", RELATION_DESCRIBED_BY, "descr1"); + + // aria-describedby, multiple relations + testRelation("descr2", RELATION_DESCRIPTION_FOR, "checkbox5"); + testRelation("descr3", RELATION_DESCRIPTION_FOR, "checkbox5"); + testRelation("checkbox5", RELATION_DESCRIBED_BY, ["descr2", "descr3"]); + + // xul:description@control + testRelation("descr4", RELATION_DESCRIPTION_FOR, "checkbox6"); + testRelation("checkbox6", RELATION_DESCRIBED_BY, "descr4"); + + // xul:description@control, multiple + testRelation("descr5", RELATION_DESCRIPTION_FOR, "checkbox7"); + testRelation("descr6", RELATION_DESCRIPTION_FOR, "checkbox7"); + testRelation("checkbox7", RELATION_DESCRIBED_BY, + [ "descr5", "descr6" ]); + + // aria_owns, multiple relations + testRelation("treeitem1", RELATION_NODE_CHILD_OF, "tree"); + testRelation("treeitem2", RELATION_NODE_CHILD_OF, "tree"); + + // 'node child of' relation for outlineitem role + testRelation("treeitem3", RELATION_NODE_CHILD_OF, "tree"); + testRelation("treeitem4", RELATION_NODE_CHILD_OF, "tree"); + testRelation("treeitem5", RELATION_NODE_CHILD_OF, "treeitem4"); + + // no relation node_child_of for accessible contained in an unexpected + // parent + testRelation("treeitem6", RELATION_NODE_CHILD_OF, null); + + // 'node child of' relation for the document having window, returns + // direct accessible parent (fixed in bug 419770). + var iframeElmObj = {}; + var iframeAcc = getAccessible("iframe", null, iframeElmObj); + var iframeDoc = iframeElmObj.value.contentDocument; + var iframeDocAcc = getAccessible(iframeDoc); + testRelation(iframeDocAcc, RELATION_NODE_CHILD_OF, iframeAcc); + + // aria-controls + getAccessible("tab"); + todo(false, + "Getting an accessible tab, otherwise relations for tabpanel aren't cached. Bug 606924 will fix that."); + testRelation("tabpanel", RELATION_CONTROLLED_BY, "tab"); + testRelation("tab", RELATION_CONTROLLER_FOR, "tabpanel"); + + // aria-controls, multiple relations + testRelation("lr1", RELATION_CONTROLLED_BY, "button"); + testRelation("lr2", RELATION_CONTROLLED_BY, "button"); + testRelation("button", RELATION_CONTROLLER_FOR, ["lr1", "lr2"]); + + // aria-flowto + testRelation("flowto", RELATION_FLOWS_TO, "flowfrom"); + testRelation("flowfrom", RELATION_FLOWS_FROM, "flowto"); + + // aria-flowto, multiple relations + testRelation("flowto1", RELATION_FLOWS_TO, ["flowfrom1", "flowfrom2"]); + testRelation("flowfrom1", RELATION_FLOWS_FROM, "flowto1"); + testRelation("flowfrom2", RELATION_FLOWS_FROM, "flowto1"); + + // 'labelled by'/'label for' relation for xul:groupbox and xul:label + var groupboxAcc = getAccessible("groupbox"); + var labelAcc = groupboxAcc.firstChild; + testRelation(labelAcc, RELATION_LABEL_FOR, groupboxAcc); + testRelation(groupboxAcc, RELATION_LABELLED_BY, labelAcc); + + // 'labelled by'/'label for' relations for xul:tab and xul:tabpanel + // (fixed in bug 366527) + testRelation("tabpanel1", RELATION_LABELLED_BY, "tab1"); + testRelation("tab1", RELATION_LABEL_FOR, "tabpanel1"); + testRelation("tabpanel2", RELATION_LABELLED_BY, "tab2"); + testRelation("tab2", RELATION_LABEL_FOR, "tabpanel2"); + testRelation("tabpanel3", RELATION_LABELLED_BY, "tab3"); + testRelation("tab3", RELATION_LABEL_FOR, "tabpanel3"); + + // finish test + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <vbox style="overflow: auto;" flex="1"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=475298" + title="mochitests for accessible relations"> + Mozilla Bug 475298 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673389" + title="node_child_of on an item not in a proper container"> + Mozilla Bug 67389 + </a><br/> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <label id="label1" control="checkbox1">label</label> + <checkbox id="checkbox1"/> + + <label id="label1_1" control="checkbox1_1">label</label> + <label id="label1_2" control="checkbox1_1">label</label> + <checkbox id="checkbox1_1"/> + + <description id="label2">label</description> + <description role="checkbox" id="checkbox2" aria-labelledby="label2"/> + + <description id="label3">label</description> + <description id="label4">label</description> + <description role="checkbox" id="checkbox3" + aria-labelledby="label3 label4"/> + + <label id="label_input" control="input">label</label> + <html:input id="input"/> + + <description id="descr1">description</description> + <description role="checkbox" id="checkbox4" aria-describedby="descr1"/> + + <description id="descr2">label</description> + <description id="descr3">label</description> + <description role="checkbox" id="checkbox5" + aria-describedby="descr2 descr3"/> + + <description id="descr4" control="checkbox6">description</description> + <checkbox id="checkbox6"/> + + <description id="descr5" control="checkbox7">description</description> + <description id="descr6" control="checkbox7">description</description> + <checkbox id="checkbox7"/> + + <description role="treeitem" id="treeitem1">Yellow</description> + <description role="treeitem" id="treeitem2">Orange</description> + <vbox id="tree" role="tree" aria-owns="treeitem1 treeitem2"> + <description role="treeitem" id="treeitem3">Blue</description> + <description role="treeitem" id="treeitem4" aria-level="1">Green</description> + <description role="treeitem" id="treeitem5" aria-level="2">Light green</description> + </vbox> + + <description role="treeitem" id="treeitem6">Dark green</description> + + <iframe id="iframe"/> + + <hbox id="tablist" role="tablist"> + <description id="tab" role="tab" aria-controls="tabpanel">tab</description> + </hbox> + <description id="tabpanel" role="tabpanel">tabpanel</description> + + <description id="lr1" aria-live="assertive">1</description> + <description id="lr2" aria-live="assertive">a</description> + <button id="button" aria-controls="lr1 lr2" label="button" + oncommand="getNode('lr1').textContent += '1'; getNode('lr2').textContent += 'a';"/> + + <description id="flowto1" aria-flowto="flowfrom1 flowfrom2">flow to</description> + <description id="flowfrom1">flow from</description> + <description id="flowfrom2">flow from</description> + + <description id="flowto" aria-flowto="flowfrom">flow to</description> + <description id="flowfrom">flow from</description> + + <groupbox id="groupbox"> + <label value="caption"/> + </groupbox> + + <tabbox> + <tabs> + <tab label="tab1" id="tab1"/> + <tab label="tab2" id="tab2" linkedpanel="tabpanel2"/> + <tab label="tab3" id="tab3" linkedpanel="tabpanel3"/> + </tabs> + <tabpanels> + <tabpanel id="tabpanel1"> + <description>tabpanel1</description> + </tabpanel> + <tabpanel id="tabpanel3"> + <description>tabpanel3</description> + </tabpanel> + <tabpanel id="tabpanel2"> + <description>tabpanel2</description> + </tabpanel> + </tabpanels> + </tabbox> + + </vbox> +</window> diff --git a/accessible/tests/mochitest/relations/test_groupInfoUpdate.html b/accessible/tests/mochitest/relations/test_groupInfoUpdate.html new file mode 100644 index 0000000000..efca27617c --- /dev/null +++ b/accessible/tests/mochitest/relations/test_groupInfoUpdate.html @@ -0,0 +1,57 @@ +<html> +<head> + <title>Test accessible relations when AccGroupInfo updated</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function doTests() { + info("Testing NODE_CHILD_OF update after DOM removal"); + testRelation("l1i2", RELATION_NODE_CHILD_OF, "l1i1"); + let reorder = waitForEvent(EVENT_REORDER, "l1"); + getNode("l1i1").remove(); + await reorder; + testRelation("l1i2", RELATION_NODE_CHILD_OF, "l1"); + + info("Testing NODE_CHILD_OF update after aria-owns removal"); + testRelation("l2i2", RELATION_NODE_CHILD_OF, "l2i1"); + reorder = waitForEvent(EVENT_REORDER, "l2"); + // Move l2i1 out of l2 using aria-owns. + getNode("l2trash").setAttribute("aria-owns", "l2i1"); + await reorder; + testRelation("l2i2", RELATION_NODE_CHILD_OF, "l2"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body id="body"> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="l1" role="list"> + <div id="l1i1" role="listitem" aria-level="1">a</div> + <div id="l1i2" role="listitem" aria-level="2">b</div> + </div> + + <div id="l2" role="list"> + <div id="l2i1" role="listitem" aria-level="1">a</div> + <div id="l2i2" role="listitem" aria-level="2">b</div> + </div> + <div id="l2trash"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/relations/test_shadowdom.html b/accessible/tests/mochitest/relations/test_shadowdom.html new file mode 100644 index 0000000000..adb9490d99 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_shadowdom.html @@ -0,0 +1,58 @@ +<html> + +<head> + <title>Explicit content and shadow DOM content relations tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // explicit content + let label = document.getElementById("label"); + let element = document.getElementById("element"); + testRelation(label, RELATION_LABEL_FOR, element); + testRelation(element, RELATION_LABELLED_BY, label); + + // shadow DOM content + let shadowRoot = document.getElementById("shadowcontainer").shadowRoot; + let shadowLabel = shadowRoot.getElementById("label"); + let shadowElement = shadowRoot.getElementById("element"); + + testRelation(shadowLabel, RELATION_LABEL_FOR, shadowElement); + testRelation(shadowElement, RELATION_LABELLED_BY, shadowLabel); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + addA11yLoadEvent(doTest, window); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content"> + <div id="label"></div> + <div id="element" aria-labelledby="label"></div> + <div id="shadowcontainer"></div> + <script> + let shadowRoot = document.getElementById("shadowcontainer"). + attachShadow({mode: "open"}); + shadowRoot.innerHTML = + `<div id="label"></div><div id="element" aria-labelledby="label"></div>`; + </script> + </div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/relations/test_tabbrowser.xhtml b/accessible/tests/mochitest/relations/test_tabbrowser.xhtml new file mode 100644 index 0000000000..3356bc6140 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_tabbrowser.xhtml @@ -0,0 +1,109 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tabbrowser relation tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../relations.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Invoker + function testTabRelations() + { + this.eventSeq = [ + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 0), + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 1) + ]; + + this.invoke = function testTabRelations_invoke() + { + var docURIs = ["about:license", "about:mozilla"]; + tabBrowser().loadTabs(docURIs, { + inBackground: false, + replace: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + // Flush layout, so as to guarantee that the a11y tree is constructed. + browserDocument().documentElement.getBoundingClientRect(); + } + + this.finalCheck = function testTabRelations_finalCheck(aEvent) + { + //////////////////////////////////////////////////////////////////////// + // 'labelled by'/'label for' relations for xul:tab and xul:tabpanel + + var tabs = Array.from(tabBrowser().tabContainer.allTabs); + // For preloaded tabs, there might be items in this array where this relation + // doesn't hold, so just deal with that: + var panels = tabs.map(t => t.linkedBrowser.closest("tabpanels > *")); + + testRelation(panels[0], RELATION_LABELLED_BY, tabs[0]); + testRelation(tabs[0], RELATION_LABEL_FOR, panels[0]); + testRelation(panels[1], RELATION_LABELLED_BY, tabs[1]); + testRelation(tabs[1], RELATION_LABEL_FOR, panels[1]); + } + + this.getID = function testTabRelations_getID() + { + return "relations of tabs"; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTest() + { + // Load documents into tabs and wait for DocLoadComplete events caused by + // these documents load before we start the test. + + gQueue = new eventQueue(); + + gQueue.push(new testTabRelations()); + gQueue.onFinish = function() { closeBrowserWindow(); } + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTest); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=552944" + title="No relationship between tabs and associated property page in new tabbrowser construct"> + Mozilla Bug 552944 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="eventdump"></vbox> + </vbox> + +</window> + diff --git a/accessible/tests/mochitest/relations/test_tree.xhtml b/accessible/tests/mochitest/relations/test_tree.xhtml new file mode 100644 index 0000000000..7c309f0956 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_tree.xhtml @@ -0,0 +1,105 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tree relations tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../relations.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + var treeNode = getNode("tree"); + + var tree = getAccessible(treeNode); + var treeitem1 = tree.firstChild.nextSibling; + testRelation(treeitem1, RELATION_NODE_CHILD_OF, [tree]); + + var treeitem2 = treeitem1.nextSibling; + testRelation(treeitem2, RELATION_NODE_CHILD_OF, [tree]); + + var treeitem3 = treeitem2.nextSibling; + testRelation(treeitem3, RELATION_NODE_CHILD_OF, [treeitem2]); + + var treeitem4 = treeitem3.nextSibling; + testRelation(treeitem4, RELATION_NODE_CHILD_OF, [treeitem2]); + + var treeitem5 = treeitem4.nextSibling; + testRelation(treeitem5, RELATION_NODE_CHILD_OF, [tree]); + + var treeitem6 = treeitem5.nextSibling; + testRelation(treeitem6, RELATION_NODE_CHILD_OF, [tree]); + + testRelation(tree, RELATION_NODE_PARENT_OF, + [treeitem1, treeitem2, treeitem5, treeitem6]); + testRelation(treeitem2, RELATION_NODE_PARENT_OF, + [treeitem3, treeitem4]); + + // treeitems and treecells shouldn't pick up relations from tree + testRelation(treeitem1, RELATION_LABELLED_BY, null); + testRelation(treeitem1.firstChild, RELATION_LABELLED_BY, null); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "tree", new nsTreeTreeView()); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Bug 503727 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=527461" + title="Implement RELATION_NODE_PARENT_OF"> + Bug 527461 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=691248" + title="XUL tree items shouldn't pick up relations from XUL tree"> + Bug 691248 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <label control="tree" value="It's a tree"/> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="column2"/> + </treecols> + <treechildren/> + </tree> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/relations/test_ui_modalprompt.html b/accessible/tests/mochitest/relations/test_ui_modalprompt.html new file mode 100644 index 0000000000..a05b273d86 --- /dev/null +++ b/accessible/tests/mochitest/relations/test_ui_modalprompt.html @@ -0,0 +1,111 @@ +<html> + +<head> + <title>Modal prompts</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + SpecialPowers.pushPrefEnv({ + set: [["prompts.contentPromptSubDialog", false]], + }); + function showAlert() { + this.eventSeq = [ + { + type: EVENT_SHOW, + match(aEvent) { + return aEvent.accessible.role == ROLE_DIALOG; + }, + }, + ]; + + this.invoke = function showAlert_invoke() { + window.setTimeout( + function() { + currentTabDocument().defaultView.alert("hello"); + }, 0); + }; + + this.check = function showAlert_finalCheck(aEvent) { + if(aEvent.type === EVENT_HIDE) { + return; + } + var dialog = aEvent.accessible.DOMNode; + var info = dialog.querySelector(".tabmodalprompt-infoBody"); + testRelation(info, RELATION_DESCRIPTION_FOR, dialog); + testRelation(dialog, RELATION_DESCRIBED_BY, info); + }; + + this.getID = function showAlert_getID() { + return "show alert"; + }; + } + + function closeAlert() { + this.eventSeq = [ + { + type: EVENT_HIDE, + match(aEvent) { + return aEvent.accessible.role == ROLE_DIALOG; + }, + }, + ]; + + this.invoke = function showAlert_invoke() { + synthesizeKey("VK_RETURN", {}, browserWindow()); + }; + + this.getID = function showAlert_getID() { + return "cleanup alert"; + }; + } + + + // gA11yEventDumpToConsole = true; // debug + + var gQueue = null; + function doTests() { + gQueue = new eventQueue(); + gQueue.push(new showAlert()); + gQueue.push(new closeAlert()); + gQueue.onFinish = function() { + closeBrowserWindow(); + }; + gQueue.invoke(); // will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTests); + </script> + +</head> + +<body id="body"> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=661293" + title="The tabmodalprompt dialog's prompt label doesn't get the text properly associated for accessibility"> + Mozilla Bug 661293 + </a> + <br> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + +</body> +</html> diff --git a/accessible/tests/mochitest/relations/test_update.html b/accessible/tests/mochitest/relations/test_update.html new file mode 100644 index 0000000000..581d592bec --- /dev/null +++ b/accessible/tests/mochitest/relations/test_update.html @@ -0,0 +1,213 @@ +<html> + +<head> + <title>Test updating of accessible relations</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../relations.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function testRelated(aRelAttr, aHostRelation, aDependentRelation, + aHostID, aHostNodeID, aDependent1ID, aDependent2ID) { + // no attribute + testRelation(aDependent1ID, aDependentRelation, null); + testRelation(aDependent2ID, aDependentRelation, null); + if (aHostRelation) + testRelation(aHostID, aHostRelation, null); + + // set attribute + getNode(aHostNodeID).setAttribute(aRelAttr, aDependent1ID); + testRelation(aDependent1ID, aDependentRelation, aHostID); + testRelation(aDependent2ID, aDependentRelation, null); + if (aHostRelation) + testRelation(aHostID, aHostRelation, aDependent1ID); + + // change attribute + getNode(aHostNodeID).setAttribute(aRelAttr, aDependent2ID); + testRelation(aDependent1ID, aDependentRelation, null); + testRelation(aDependent2ID, aDependentRelation, aHostID); + if (aHostRelation) + testRelation(aHostID, aHostRelation, aDependent2ID); + + // remove attribute + getNode(aHostNodeID).removeAttribute(aRelAttr); + testRelation(aDependent1ID, aDependentRelation, null); + testRelation(aDependent2ID, aDependentRelation, null); + if (aHostRelation) + testRelation(aHostID, aHostRelation, null); + } + + function insertRelated(aHostRelAttr, aDependentID, aInsertHostFirst, + aHostRelation, aDependentRelation) { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, document), + ]; + + this.invoke = function insertRelated_invoke() { + this.hostNode = document.createElement("div"); + this.hostNode.setAttribute(aHostRelAttr, aDependentID); + + this.dependentNode = document.createElement("div"); + this.dependentNode.setAttribute("id", aDependentID); + + if (aInsertHostFirst) { + document.body.appendChild(this.hostNode); + document.body.appendChild(this.dependentNode); + } else { + document.body.appendChild(this.dependentNode); + document.body.appendChild(this.hostNode); + } + }; + + this.finalCheck = function insertRelated_finalCheck() { + testRelation(this.dependentNode, aDependentRelation, this.hostNode); + if (aHostRelation) + testRelation(this.hostNode, aHostRelation, this.dependentNode); + }; + + this.getID = function insertRelated_getID() { + return "Insert " + aHostRelAttr + "='" + aDependentID + "' node" + + (aInsertHostFirst ? " before" : "after") + " dependent node"; + }; + } + + /** + * Relative accessible recreation shouldn't break accessible relations. + * Note: modify this case if the invoke function doesn't change accessible + * tree due to changes in layout module. It can be changed on any case + * when accessibles are recreated. + */ + function recreateRelatives(aContainerID, aLabelID, aElmID) { + this.containerNode = getNode(aContainerID); + this.container = getNode(this.containerNode); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.container), + new invokerChecker(EVENT_SHOW, this.containerNode), + ]; + + this.invoke = function recreateRelatives_invoke() { + testRelation(aLabelID, RELATION_LABEL_FOR, aElmID); + testRelation(aElmID, RELATION_LABELLED_BY, aLabelID); + + this.containerNode.setAttribute('role', 'group'); + }; + + this.finalCheck = function recreateRelatives_finalCheck() { + testRelation(aLabelID, RELATION_LABEL_FOR, aElmID); + testRelation(aElmID, RELATION_LABELLED_BY, aLabelID); + }; + + this.getID = function recreateRelatives_getID() { + return "recreate relatives "; + }; + } + + // gA11yEventDumpToConsole = true; // debug + + var gQueue = null; + + function doTest() { + // Relation updates on ARIA attribute changes. + testRelated("aria-labelledby", + RELATION_LABELLED_BY, RELATION_LABEL_FOR, + "host", "host", "dependent1", "dependent2"); + + testRelated("aria-describedby", + RELATION_DESCRIBED_BY, RELATION_DESCRIPTION_FOR, + "host", "host", "dependent1", "dependent2"); + + testRelated("aria-controls", + RELATION_CONTROLLER_FOR, RELATION_CONTROLLED_BY, + "host", "host", "dependent1", "dependent2"); + + testRelated("aria-flowto", + RELATION_FLOWS_TO, RELATION_FLOWS_FROM, + "host", "host", "dependent1", "dependent2"); + + // Document relation updates on ARIA attribute change. + testRelated("aria-labelledby", + RELATION_LABELLED_BY, RELATION_LABEL_FOR, + document, "body", "dependent1", "dependent2"); + + // Insert related accessibles into tree. + gQueue = new eventQueue(); + gQueue.push(new insertRelated("aria-labelledby", "dependent3", true, + RELATION_LABELLED_BY, RELATION_LABEL_FOR)); + gQueue.push(new insertRelated("aria-labelledby", "dependent4", false, + RELATION_LABELLED_BY, RELATION_LABEL_FOR)); + + gQueue.push(new insertRelated("aria-describedby", "dependent5", true, + RELATION_DESCRIBED_BY, + RELATION_DESCRIPTION_FOR)); + gQueue.push(new insertRelated("aria-describedby", "dependent6", false, + RELATION_DESCRIBED_BY, + RELATION_DESCRIPTION_FOR)); + + gQueue.push(new insertRelated("aria-controls", "dependent9", true, + RELATION_CONTROLLER_FOR, + RELATION_CONTROLLED_BY)); + gQueue.push(new insertRelated("aria-controls", "dependent10", false, + RELATION_CONTROLLER_FOR, + RELATION_CONTROLLED_BY)); + + gQueue.push(new insertRelated("aria-flowto", "dependent11", true, + RELATION_FLOWS_TO, RELATION_FLOWS_FROM)); + gQueue.push(new insertRelated("aria-flowto", "dependent12", false, + RELATION_FLOWS_TO, RELATION_FLOWS_FROM)); + + // Update relations when accessibles are recreated + gQueue.push(new recreateRelatives("container", "label", "input")); + + gQueue.invoke(); // will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body id="body"> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=573469" + title="Cache relations defined by ARIA attributes"> + Mozilla Bug 573469 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=631068" + title="Accessible recreation breaks relations"> + Mozilla Bug 631068 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=635346" + title="Allow relations for document defined on document content"> + Mozilla Bug 635346 + </a> + <br> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="dependent1">label</div> + <div id="dependent2">label2</div> + <div role="checkbox" id="host"></div> + + <form id="container" style="overflow: hidden;"> + <label for="input" id="label">label</label> + <input id="input"> + </form> +</body> +</html> diff --git a/accessible/tests/mochitest/role.js b/accessible/tests/mochitest/role.js new file mode 100644 index 0000000000..b50ad37a73 --- /dev/null +++ b/accessible/tests/mochitest/role.js @@ -0,0 +1,197 @@ +/* import-globals-from common.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Role constants + +const ROLE_ALERT = nsIAccessibleRole.ROLE_ALERT; +const ROLE_ARTICLE = nsIAccessibleRole.ROLE_ARTICLE; +const ROLE_ANIMATION = nsIAccessibleRole.ROLE_ANIMATION; +const ROLE_APPLICATION = nsIAccessibleRole.ROLE_APPLICATION; +const ROLE_APP_ROOT = nsIAccessibleRole.ROLE_APP_ROOT; +const ROLE_AUTOCOMPLETE = nsIAccessibleRole.ROLE_AUTOCOMPLETE; +const ROLE_BLOCKQUOTE = nsIAccessibleRole.ROLE_BLOCKQUOTE; +const ROLE_BUTTONDROPDOWNGRID = nsIAccessibleRole.ROLE_BUTTONDROPDOWNGRID; +const ROLE_CANVAS = nsIAccessibleRole.ROLE_CANVAS; +const ROLE_CAPTION = nsIAccessibleRole.ROLE_CAPTION; +const ROLE_CELL = nsIAccessibleRole.ROLE_CELL; +const ROLE_CHECKBUTTON = nsIAccessibleRole.ROLE_CHECKBUTTON; +const ROLE_CHECK_MENU_ITEM = nsIAccessibleRole.ROLE_CHECK_MENU_ITEM; +const ROLE_CHROME_WINDOW = nsIAccessibleRole.ROLE_CHROME_WINDOW; +const ROLE_CODE = nsIAccessibleRole.ROLE_CODE; +const ROLE_COLUMNHEADER = nsIAccessibleRole.ROLE_COLUMNHEADER; +const ROLE_COMBOBOX = nsIAccessibleRole.ROLE_COMBOBOX; +const ROLE_COMBOBOX_LIST = nsIAccessibleRole.ROLE_COMBOBOX_LIST; +const ROLE_COMBOBOX_OPTION = nsIAccessibleRole.ROLE_COMBOBOX_OPTION; +const ROLE_COMMENT = nsIAccessibleRole.ROLE_COMMENT; +const ROLE_CONTENT_DELETION = nsIAccessibleRole.ROLE_CONTENT_DELETION; +const ROLE_CONTENT_INSERTION = nsIAccessibleRole.ROLE_CONTENT_INSERTION; +const ROLE_DATE_EDITOR = nsIAccessibleRole.ROLE_DATE_EDITOR; +const ROLE_DEFINITION = nsIAccessibleRole.ROLE_DEFINITION; +const ROLE_DEFINITION_LIST = nsIAccessibleRole.ROLE_DEFINITION_LIST; +const ROLE_DETAILS = nsIAccessibleRole.ROLE_DETAILS; +const ROLE_DIAGRAM = nsIAccessibleRole.ROLE_DIAGRAM; +const ROLE_DIALOG = nsIAccessibleRole.ROLE_DIALOG; +const ROLE_DOCUMENT = nsIAccessibleRole.ROLE_DOCUMENT; +const ROLE_EDITCOMBOBOX = nsIAccessibleRole.ROLE_EDITCOMBOBOX; +const ROLE_EMBEDDED_OBJECT = nsIAccessibleRole.ROLE_EMBEDDED_OBJECT; +const ROLE_ENTRY = nsIAccessibleRole.ROLE_ENTRY; +const ROLE_EQUATION = nsIAccessibleRole.ROLE_EQUATION; +const ROLE_FIGURE = nsIAccessibleRole.ROLE_FIGURE; +const ROLE_FOOTER = nsIAccessibleRole.ROLE_FOOTER; +const ROLE_FOOTNOTE = nsIAccessibleRole.ROLE_FOOTNOTE; +const ROLE_FLAT_EQUATION = nsIAccessibleRole.ROLE_FLAT_EQUATION; +const ROLE_FORM = nsIAccessibleRole.ROLE_FORM; +const ROLE_FORM_LANDMARK = nsIAccessibleRole.ROLE_FORM_LANDMARK; +const ROLE_GRAPHIC = nsIAccessibleRole.ROLE_GRAPHIC; +const ROLE_GRID_CELL = nsIAccessibleRole.ROLE_GRID_CELL; +const ROLE_GROUPING = nsIAccessibleRole.ROLE_GROUPING; +const ROLE_HEADER = nsIAccessibleRole.ROLE_HEADER; +const ROLE_HEADING = nsIAccessibleRole.ROLE_HEADING; +const ROLE_IMAGE_MAP = nsIAccessibleRole.ROLE_IMAGE_MAP; +const ROLE_INTERNAL_FRAME = nsIAccessibleRole.ROLE_INTERNAL_FRAME; +const ROLE_LABEL = nsIAccessibleRole.ROLE_LABEL; +const ROLE_LANDMARK = nsIAccessibleRole.ROLE_LANDMARK; +const ROLE_LINK = nsIAccessibleRole.ROLE_LINK; +const ROLE_LIST = nsIAccessibleRole.ROLE_LIST; +const ROLE_LISTBOX = nsIAccessibleRole.ROLE_LISTBOX; +const ROLE_LISTITEM = nsIAccessibleRole.ROLE_LISTITEM; +const ROLE_LISTITEM_MARKER = nsIAccessibleRole.ROLE_LISTITEM_MARKER; +const ROLE_MARK = nsIAccessibleRole.ROLE_MARK; +const ROLE_MATHML_MATH = nsIAccessibleRole.ROLE_MATHML_MATH; +const ROLE_MATHML_IDENTIFIER = nsIAccessibleRole.ROLE_MATHML_IDENTIFIER; +const ROLE_MATHML_NUMBER = nsIAccessibleRole.ROLE_MATHML_NUMBER; +const ROLE_MATHML_OPERATOR = nsIAccessibleRole.ROLE_MATHML_OPERATOR; +const ROLE_MATHML_TEXT = nsIAccessibleRole.ROLE_MATHML_TEXT; +const ROLE_MATHML_STRING_LITERAL = nsIAccessibleRole.ROLE_MATHML_STRING_LITERAL; +const ROLE_MATHML_GLYPH = nsIAccessibleRole.ROLE_MATHML_GLYPH; +const ROLE_MATHML_ROW = nsIAccessibleRole.ROLE_MATHML_ROW; +const ROLE_MATHML_FRACTION = nsIAccessibleRole.ROLE_MATHML_FRACTION; +const ROLE_MATHML_SQUARE_ROOT = nsIAccessibleRole.ROLE_MATHML_SQUARE_ROOT; +const ROLE_MATHML_ROOT = nsIAccessibleRole.ROLE_MATHML_ROOT; +const ROLE_MATHML_FENCED = nsIAccessibleRole.ROLE_MATHML_FENCED; +const ROLE_MATHML_ENCLOSED = nsIAccessibleRole.ROLE_MATHML_ENCLOSED; +const ROLE_MATHML_STYLE = nsIAccessibleRole.ROLE_MATHML_STYLE; +const ROLE_MATHML_SUB = nsIAccessibleRole.ROLE_MATHML_SUB; +const ROLE_MATHML_SUP = nsIAccessibleRole.ROLE_MATHML_SUP; +const ROLE_MATHML_SUB_SUP = nsIAccessibleRole.ROLE_MATHML_SUB_SUP; +const ROLE_MATHML_UNDER = nsIAccessibleRole.ROLE_MATHML_UNDER; +const ROLE_MATHML_OVER = nsIAccessibleRole.ROLE_MATHML_OVER; +const ROLE_MATHML_UNDER_OVER = nsIAccessibleRole.ROLE_MATHML_UNDER_OVER; +const ROLE_MATHML_MULTISCRIPTS = nsIAccessibleRole.ROLE_MATHML_MULTISCRIPTS; +const ROLE_MATHML_TABLE = nsIAccessibleRole.ROLE_MATHML_TABLE; +const ROLE_MATHML_LABELED_ROW = nsIAccessibleRole.ROLE_MATHML_LABELED_ROW; +const ROLE_MATHML_TABLE_ROW = nsIAccessibleRole.ROLE_MATHML_TABLE_ROW; +const ROLE_MATHML_CELL = nsIAccessibleRole.ROLE_MATHML_CELL; +const ROLE_MATHML_ACTION = nsIAccessibleRole.ROLE_MATHML_ACTION; +const ROLE_MATHML_ERROR = nsIAccessibleRole.ROLE_MATHML_ERROR; +const ROLE_MATHML_STACK = nsIAccessibleRole.ROLE_MATHML_STACK; +const ROLE_MATHML_LONG_DIVISION = nsIAccessibleRole.ROLE_MATHML_LONG_DIVISION; +const ROLE_MATHML_STACK_GROUP = nsIAccessibleRole.ROLE_MATHML_STACK_GROUP; +const ROLE_MATHML_STACK_ROW = nsIAccessibleRole.ROLE_MATHML_STACK_ROW; +const ROLE_MATHML_STACK_CARRIES = nsIAccessibleRole.ROLE_MATHML_STACK_CARRIES; +const ROLE_MATHML_STACK_CARRY = nsIAccessibleRole.ROLE_MATHML_STACK_CARRY; +const ROLE_MATHML_STACK_LINE = nsIAccessibleRole.ROLE_MATHML_STACK_LINE; +const ROLE_MENUBAR = nsIAccessibleRole.ROLE_MENUBAR; +const ROLE_MENUITEM = nsIAccessibleRole.ROLE_MENUITEM; +const ROLE_MENUPOPUP = nsIAccessibleRole.ROLE_MENUPOPUP; +const ROLE_NAVIGATION = nsIAccessibleRole.ROLE_NAVIGATION; +const ROLE_NON_NATIVE_DOCUMENT = nsIAccessibleRole.ROLE_NON_NATIVE_DOCUMENT; +const ROLE_NOTHING = nsIAccessibleRole.ROLE_NOTHING; +const ROLE_NOTE = nsIAccessibleRole.ROLE_NOTE; +const ROLE_OPTION = nsIAccessibleRole.ROLE_OPTION; +const ROLE_OUTLINE = nsIAccessibleRole.ROLE_OUTLINE; +const ROLE_OUTLINEITEM = nsIAccessibleRole.ROLE_OUTLINEITEM; +const ROLE_PAGETAB = nsIAccessibleRole.ROLE_PAGETAB; +const ROLE_PAGETABLIST = nsIAccessibleRole.ROLE_PAGETABLIST; +const ROLE_PANE = nsIAccessibleRole.ROLE_PANE; +const ROLE_PARAGRAPH = nsIAccessibleRole.ROLE_PARAGRAPH; +const ROLE_PARENT_MENUITEM = nsIAccessibleRole.ROLE_PARENT_MENUITEM; +const ROLE_PASSWORD_TEXT = nsIAccessibleRole.ROLE_PASSWORD_TEXT; +const ROLE_PROGRESSBAR = nsIAccessibleRole.ROLE_PROGRESSBAR; +const ROLE_PROPERTYPAGE = nsIAccessibleRole.ROLE_PROPERTYPAGE; +const ROLE_PUSHBUTTON = nsIAccessibleRole.ROLE_PUSHBUTTON; +const ROLE_RADIOBUTTON = nsIAccessibleRole.ROLE_RADIOBUTTON; +const ROLE_RADIO_GROUP = nsIAccessibleRole.ROLE_RADIO_GROUP; +const ROLE_RADIO_MENU_ITEM = nsIAccessibleRole.ROLE_RADIO_MENU_ITEM; +const ROLE_REGION = nsIAccessibleRole.ROLE_REGION; +const ROLE_RICH_OPTION = nsIAccessibleRole.ROLE_RICH_OPTION; +const ROLE_ROW = nsIAccessibleRole.ROLE_ROW; +const ROLE_ROWHEADER = nsIAccessibleRole.ROLE_ROWHEADER; +const ROLE_SCROLLBAR = nsIAccessibleRole.ROLE_SCROLLBAR; +const ROLE_SECTION = nsIAccessibleRole.ROLE_SECTION; +const ROLE_SEPARATOR = nsIAccessibleRole.ROLE_SEPARATOR; +const ROLE_SLIDER = nsIAccessibleRole.ROLE_SLIDER; +const ROLE_SPINBUTTON = nsIAccessibleRole.ROLE_SPINBUTTON; +const ROLE_STATICTEXT = nsIAccessibleRole.ROLE_STATICTEXT; +const ROLE_STATUSBAR = nsIAccessibleRole.ROLE_STATUSBAR; +const ROLE_SUGGESTION = nsIAccessibleRole.ROLE_SUGGESTION; +const ROLE_SUMMARY = nsIAccessibleRole.ROLE_SUMMARY; +const ROLE_SWITCH = nsIAccessibleRole.ROLE_SWITCH; +const ROLE_TABLE = nsIAccessibleRole.ROLE_TABLE; +const ROLE_TERM = nsIAccessibleRole.ROLE_TERM; +const ROLE_TEXT = nsIAccessibleRole.ROLE_TEXT; +const ROLE_TEXT_CONTAINER = nsIAccessibleRole.ROLE_TEXT_CONTAINER; +const ROLE_TEXT_LEAF = nsIAccessibleRole.ROLE_TEXT_LEAF; +const ROLE_TIME_EDITOR = nsIAccessibleRole.ROLE_TIME_EDITOR; +const ROLE_TOGGLE_BUTTON = nsIAccessibleRole.ROLE_TOGGLE_BUTTON; +const ROLE_TOOLBAR = nsIAccessibleRole.ROLE_TOOLBAR; +const ROLE_TOOLTIP = nsIAccessibleRole.ROLE_TOOLTIP; +const ROLE_TREE_TABLE = nsIAccessibleRole.ROLE_TREE_TABLE; +const ROLE_WHITESPACE = nsIAccessibleRole.ROLE_WHITESPACE; + +// ////////////////////////////////////////////////////////////////////////////// +// Public methods + +/** + * Test that the role of the given accessible is the role passed in. + * + * @param aAccOrElmOrID the accessible, DOM element or ID to be tested. + * @param aRole The role that is to be expected. + */ +function testRole(aAccOrElmOrID, aRole) { + var role = getRole(aAccOrElmOrID); + is(role, aRole, "Wrong role for " + prettyName(aAccOrElmOrID) + "!"); +} + +/** + * Return the role of the given accessible. Return -1 if accessible could not + * be retrieved. + * + * @param aAccOrElmOrID [in] The accessible, DOM element or element ID the + * accessible role is being requested for. + */ +function getRole(aAccOrElmOrID) { + var acc = getAccessible(aAccOrElmOrID); + if (!acc) { + return -1; + } + + var role = -1; + try { + role = acc.role; + } catch (e) { + ok(false, "Role for " + aAccOrElmOrID + " could not be retrieved!"); + } + + return role; +} + +/** + * Analogy of SimpleTest.is function used to check the role. + */ +function isRole(aIdentifier, aRole, aMsg) { + var role = getRole(aIdentifier); + if (role == -1) { + return; + } + + if (role == aRole) { + ok(true, aMsg); + return; + } + + var got = roleToString(role); + var expected = roleToString(aRole); + + ok(false, aMsg + "got '" + got + "', expected '" + expected + "'"); +} diff --git a/accessible/tests/mochitest/role/a11y.ini b/accessible/tests/mochitest/role/a11y.ini new file mode 100644 index 0000000000..02a2b61cb1 --- /dev/null +++ b/accessible/tests/mochitest/role/a11y.ini @@ -0,0 +1,13 @@ +[DEFAULT] +support-files = + chrome_body_role_alert.xhtml + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/moz.png + +[test_aria.html] +[test_aria.xhtml] +[test_dpub_aria.html] +[test_general.html] +[test_general.xhtml] +[test_graphics_aria.html] +[test_svg.html] diff --git a/accessible/tests/mochitest/role/chrome_body_role_alert.xhtml b/accessible/tests/mochitest/role/chrome_body_role_alert.xhtml new file mode 100644 index 0000000000..29ff10fb01 --- /dev/null +++ b/accessible/tests/mochitest/role/chrome_body_role_alert.xhtml @@ -0,0 +1,6 @@ +<?xml version="1.0"?> +<!DOCTYPE html> +<html xmlns="http://www.w3.org/1999/xhtml"> + <body role="alert"> + </body> +</html> diff --git a/accessible/tests/mochitest/role/test_aria.html b/accessible/tests/mochitest/role/test_aria.html new file mode 100644 index 0000000000..7b0a23ddc1 --- /dev/null +++ b/accessible/tests/mochitest/role/test_aria.html @@ -0,0 +1,439 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test weak ARIA roles</title> + + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + + // To test initial roles on body elements, we need to use an iframe. + function testBodyRole(iframeId, role) { + let iframe = getNode(iframeId); + let doc = iframe.contentDocument; + let docAcc = getAccessible(doc); + testRole(docAcc, role); + } + + async function doTest() { + // ARIA role map. + testRole("aria_alert", ROLE_ALERT); + testRole("aria_alertdialog", ROLE_DIALOG); + testRole("aria_application", ROLE_APPLICATION); + testRole("aria_article", ROLE_ARTICLE); + testRole("aria_blockquote", ROLE_BLOCKQUOTE); + testRole("aria_button", ROLE_PUSHBUTTON); + testRole("aria_caption", ROLE_CAPTION); + testRole("aria_checkbox", ROLE_CHECKBUTTON); + testRole("aria_code", ROLE_CODE); + testRole("aria_columnheader", ROLE_COLUMNHEADER); + testRole("aria_combobox", ROLE_EDITCOMBOBOX); + testRole("aria_comment", ROLE_COMMENT); + testRole("aria_deletion", ROLE_CONTENT_DELETION); + testRole("aria_dialog", ROLE_DIALOG); + testRole("aria_directory", ROLE_LIST); + testRole("aria_document", ROLE_NON_NATIVE_DOCUMENT); + testRole("aria_form", ROLE_FORM); + testRole("aria_form_with_label", ROLE_FORM); + testRole("aria_feed", ROLE_GROUPING); + testRole("aria_figure", ROLE_FIGURE); + testRole("aria_grid", ROLE_TABLE); + testRole("aria_gridcell", ROLE_GRID_CELL); + testRole("aria_group", ROLE_GROUPING); + testRole("aria_heading", ROLE_HEADING); + testRole("aria_img", ROLE_GRAPHIC); + testRole("aria_insertion", ROLE_CONTENT_INSERTION); + testRole("aria_link", ROLE_LINK); + testRole("aria_list", ROLE_LIST); + testRole("aria_listbox", ROLE_LISTBOX); + testRole("aria_listitem", ROLE_LISTITEM); + testRole("aria_log", ROLE_TEXT); // weak role + testRole("aria_mark", ROLE_MARK); + testRole("aria_marquee", ROLE_ANIMATION); + testRole("aria_math", ROLE_FLAT_EQUATION); + testRole("aria_menu", ROLE_MENUPOPUP); + testRole("aria_menubar", ROLE_MENUBAR); + testRole("aria_menuitem", ROLE_MENUITEM); + testRole("aria_menuitemcheckbox", ROLE_CHECK_MENU_ITEM); + testRole("aria_menuitemradio", ROLE_RADIO_MENU_ITEM); + testRole("aria_note", ROLE_NOTE); + testRole("aria_paragraph", ROLE_PARAGRAPH); + testRole("aria_presentation", ROLE_TEXT); // weak role + testRole("aria_progressbar", ROLE_PROGRESSBAR); + testRole("aria_radio", ROLE_RADIOBUTTON); + testRole("aria_radiogroup", ROLE_RADIO_GROUP); + testRole("aria_region_no_name", ROLE_TEXT); + testRole("aria_region_has_label", ROLE_REGION); + testRole("aria_region_has_labelledby", ROLE_REGION); + testRole("aria_region_has_title", ROLE_REGION); + testRole("aria_region_empty_name", ROLE_TEXT); + testRole("aria_row", ROLE_ROW); + testRole("aria_rowheader", ROLE_ROWHEADER); + testRole("aria_scrollbar", ROLE_SCROLLBAR); + testRole("aria_searchbox", ROLE_ENTRY); + testRole("aria_separator", ROLE_SEPARATOR); + testRole("aria_slider", ROLE_SLIDER); + testRole("aria_spinbutton", ROLE_SPINBUTTON); + testRole("aria_status", ROLE_STATUSBAR); + testRole("aria_suggestion", ROLE_SUGGESTION); + testRole("aria_switch", ROLE_SWITCH); + testRole("aria_tab", ROLE_PAGETAB); + testRole("aria_tablist", ROLE_PAGETABLIST); + testRole("aria_tabpanel", ROLE_PROPERTYPAGE); + testRole("aria_term", ROLE_TERM); + testRole("aria_textbox", ROLE_ENTRY); + testRole("aria_timer", ROLE_TEXT); // weak role + testRole("aria_toolbar", ROLE_TOOLBAR); + testRole("aria_tooltip", ROLE_TOOLTIP); + testRole("aria_tree", ROLE_OUTLINE); + testRole("aria_treegrid", ROLE_TREE_TABLE); + testRole("aria_treeitem", ROLE_OUTLINEITEM); + + // Note: + // The phrase "weak foo" here means that there is no good foo-to-platform + // role mapping. Similarly "strong foo" means there is a good foo-to- + // platform role mapping. + + testRole("articlemain", ROLE_LANDMARK); + testRole("articleform", ROLE_FORM); + + // Test article exposed as article + testRole("testArticle", ROLE_ARTICLE); + + // weak roles that are forms of "live regions" + testRole("log_table", ROLE_TABLE); + testRole("timer_div", ROLE_SECTION); + + // other roles that are forms of "live regions" + testRole("marquee_h1", ROLE_ANIMATION); + + // strong landmark + testRole("application", ROLE_APPLICATION); + testRole("form", ROLE_FORM); + testRole("application_table", ROLE_APPLICATION); + + // landmarks + let landmarks = ["banner", "complementary", "contentinfo", + "main", "navigation", "search"]; + for (const l in landmarks) { + testRole(landmarks[l], ROLE_LANDMARK); + } + + for (const l in landmarks) { + let id = landmarks[l] + "_table"; + testRole(id, ROLE_LANDMARK); + + let accessibleTable = getAccessible(id, [nsIAccessibleTable], null, + DONOTFAIL_IF_NO_INTERFACE); + ok(!!accessibleTable, "landmarked table should have nsIAccessibleTable"); + + if (accessibleTable) + is(accessibleTable.getCellAt(0, 0).firstChild.name, "hi", "no cell"); + } + + // //////////////////////////////////////////////////////////////////////// + // test gEmptyRoleMap + testRole("buttontable_row", ROLE_TEXT_CONTAINER); + testRole("buttontable_cell", ROLE_TEXT_CONTAINER); + + // abstract roles + var abstract_roles = ["composite", "landmark", "structure", "widget", + "window", "input", "range", "select", "section", + "sectionhead"]; + for (const a in abstract_roles) + testRole(abstract_roles[a], ROLE_SECTION); + + // //////////////////////////////////////////////////////////////////////// + // roles transformed by ARIA state attributes + testRole("togglebutton", ROLE_TOGGLE_BUTTON); + testRole("implicit_gridcell", ROLE_GRID_CELL); + + // //////////////////////////////////////////////////////////////////////// + // ignore unknown roles, take first known + testRole("unknown_roles", ROLE_PUSHBUTTON); + + // //////////////////////////////////////////////////////////////////////// + // misc roles + testRole("note", ROLE_NOTE); + testRole("scrollbar", ROLE_SCROLLBAR); + testRole("dir", ROLE_LIST); + + // //////////////////////////////////////////////////////////////////////// + // test document role map update + var testDoc = getAccessible(document, [nsIAccessibleDocument]); + testRole(testDoc, ROLE_DOCUMENT); + document.body.setAttribute("role", "application"); + testRole(testDoc, ROLE_APPLICATION); + document.body.setAttribute("role", "dialog"); + testRole(testDoc, ROLE_DIALOG); + // Other roles aren't valid on body elements. + document.body.setAttribute("role", "document"); + testRole(testDoc, ROLE_DOCUMENT); + document.body.setAttribute("role", "button"); + testRole(testDoc, ROLE_DOCUMENT); + document.body.setAttribute("role", "main"); + testRole(testDoc, ROLE_DOCUMENT); + + // Test equation image + testRole("img_eq", ROLE_FLAT_EQUATION); + + // Test textual equation + testRole("txt_eq", ROLE_FLAT_EQUATION); + + // Test initial ARIA roles on the body element. + testBodyRole("iframe_aria_application", ROLE_APPLICATION); + testBodyRole("iframe_aria_dialog", ROLE_DIALOG); + // role="alert" is valid on the body of chrome documents. + let win = Services.ww.openWindow( + null, + "chrome://mochitests/content/a11y/accessible/tests/mochitest/role/chrome_body_role_alert.xhtml", + "_blank", + "chrome", + [] + ); + await new Promise(resolve => addA11yLoadEvent(resolve, win)); + testRole(win.document, ROLE_ALERT); + win.close(); + // Other roles aren't valid on body elements. + testBodyRole("iframe_aria_document", ROLE_DOCUMENT); + testBodyRole("iframe_aria_button", ROLE_DOCUMENT); + testBodyRole("iframe_aria_main", ROLE_DOCUMENT); + // role="alert" is not valid on the body of content documents. + testBodyRole("iframe_aria_alert", ROLE_DOCUMENT); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=428479">Mozilla Bug 428479</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=429666">Mozilla Bug 429666</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=481114">Mozilla Bug 481114</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=469688">Mozilla Bug 469688</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=469688">Mozilla Bug 520188</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529289">Mozilla Bug 529289</a> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=529289">Mozilla Bug 607219</a> + <a target="_blank" + title="HTML buttons with aria-pressed not exposing IA2 TOGGLE_BUTTON role" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=725432"> + Bug 725432 + </a> + <a target="_blank" + title="Map ARIA role FORM" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=735645"> + Bug 735645 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136563" + title="Support ARIA 1.1 switch role"> + Bug 1136563 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1121518" + title="Support ARIA 1.1 searchbox role"> + Bug 1121518 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1356049" + title="Map ARIA figure role"> + Bug 1356049 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <span id="aria_alert" role="alert"></span> + <span id="aria_alertdialog" role="alertdialog"></span> + <span id="aria_application" role="application"></span> + <span id="aria_article" role="article"></span> + <span id="aria_blockquote" role="blockquote"></span> + <span id="aria_button" role="button"></span> + <span id="aria_caption" role="caption"></span> + <span id="aria_checkbox" role="checkbox"></span> + <span id="aria_code" role="code"></span> + <span id="aria_columnheader" role="columnheader"></span> + <span id="aria_combobox" role="combobox"></span> + <span id="aria_comment" role="comment"></span> + <span id="aria_deletion" role="deletion"></span> + <span id="aria_dialog" role="dialog"></span> + <span id="aria_directory" role="directory"></span> + <span id="aria_document" role="document"></span> + <span id="aria_form" role="form"></span> + <span id="aria_form_with_label" role="form" aria-label="Label"></span> + <span id="aria_feed" role="feed"></span> + <span id="aria_figure" role="figure"></span> + <span id="aria_grid" role="grid"></span> + <span id="aria_gridcell" role="gridcell"></span> + <span id="aria_group" role="group"></span> + <span id="aria_heading" role="heading"></span> + <span id="aria_img" role="img"></span> + <span id="aria_insertion" role="insertion"></span> + <span id="aria_link" role="link"></span> + <span id="aria_list" role="list"></span> + <span id="aria_listbox" role="listbox"></span> + <span id="aria_listitem" role="listitem"></span> + <span id="aria_log" role="log"></span> + <span id="aria_mark" role="mark"></span> + <span id="aria_marquee" role="marquee"></span> + <span id="aria_math" role="math"></span> + <span id="aria_menu" role="menu"></span> + <span id="aria_menubar" role="menubar"></span> + <span id="aria_menuitem" role="menuitem"></span> + <span id="aria_menuitemcheckbox" role="menuitemcheckbox"></span> + <span id="aria_menuitemradio" role="menuitemradio"></span> + <span id="aria_note" role="note"></span> + <span id="aria_paragraph" role="paragraph"></span> + <span id="aria_presentation" role="presentation" tabindex="0"></span> + <span id="aria_progressbar" role="progressbar"></span> + <span id="aria_radio" role="radio"></span> + <span id="aria_radiogroup" role="radiogroup"></span> + <span id="aria_region_no_name" role="region"></span> + <span id="aria_region_has_label" role="region" aria-label="label"></span> + <span id="aria_region_has_labelledby" role="region" aria-labelledby="label"><span id="label" aria-label="label"></span> + <span id="aria_region_has_title" role="region" title="title"></span> + <span id="aria_region_empty_name" role="region" aria-label="" title="" aria-labelledby="empty"></span><span id="empty"></span> + <span id="aria_row" role="row"></span> + <span id="aria_rowheader" role="rowheader"></span> + <span id="aria_scrollbar" role="scrollbar"></span> + <span id="aria_searchbox" role="textbox"></span> + <span id="aria_separator" role="separator"></span> + <span id="aria_slider" role="slider"></span> + <span id="aria_spinbutton" role="spinbutton"></span> + <span id="aria_status" role="status"></span> + <span id="aria_suggestion" role="suggestion"></span> + <span id="aria_switch" role="switch"></span> + <span id="aria_tab" role="tab"></span> + <span id="aria_tablist" role="tablist"></span> + <span id="aria_tabpanel" role="tabpanel"></span> + <span id="aria_term" role="term"></span> + <span id="aria_textbox" role="textbox"></span> + <span id="aria_timer" role="timer"></span> + <span id="aria_toolbar" role="toolbar"></span> + <span id="aria_tooltip" role="tooltip"></span> + <span id="aria_tree" role="tree"></span> + <span id="aria_treegrid" role="treegrid"></span> + <span id="aria_treeitem" role="treeitem"></span> + + <article id="articlemain" role="main">a main area</article> + <article id="articleform" role="form">a form area</article> + + <div id="testArticle" role="article" title="Test article"> + <p>This is a paragraph inside the article.</p> + </div> + + <!-- "live" roles --> + <table role="log" id="log_table"> + <tr><td>Table based log</td></tr> + </table> + <h1 role="marquee" id="marquee_h1">marquee</h1> + <div role="timer" id="timer_div">timer</div> + + <!-- landmarks --> + <div role="application" id="application">application</div> + <div role="form" id="form">form</div> + + <!-- weak landmarks --> + <div role="banner" id="banner">banner</div> + <div role="complementary" id="complementary">complementary</div> + <div role="contentinfo" id="contentinfo">contentinfo</div> + <div role="main" id="main">main</div> + <div role="navigation" id="navigation">navigation</div> + <div role="search" id="search">search</div> + + <!-- landmarks are tables --> + <table role="application" id="application_table">application table + <tr><td>hi<td></tr></table> + <table role="banner" id="banner_table">banner table + <tr><td>hi<td></tr></table> + <table role="complementary" id="complementary_table">complementary table + <tr><td>hi<td></tr></table> + <table role="contentinfo" id="contentinfo_table">contentinfo table + <tr><td>hi<td></tr></table> + <table role="main" id="main_table">main table + <tr><td>hi<td></tr></table> + <table role="navigation" id="navigation_table">navigation table + <tr><td>hi<td></tr></table> + <table role="search" id="search_table">search table + <tr><td>hi<td></tr></table> + + <!-- test gEmptyRoleMap --> + <table role="button"> + <tr id="buttontable_row"> + <td id="buttontable_cell">cell</td> + </tr> + </table> + + <!-- user agents must not map abstract roles to platform API --> + <!-- test abstract base type roles --> + <div role="composite" id="composite">composite</div> + <div role="landmark" id="landmark">landmark</div> + <div role="roletype" id="roletype">roletype</div> + <div role="structure" id="structure">structure</div> + <div role="widget" id="widget">widget</div> + <div role="window" id="window">window</div> + <!-- test abstract input roles --> + <div role="input" id="input">input</div> + <div role="range" id="range">range</div> + <div role="select" id="select">select</div> + <!-- test abstract structure roles --> + <div role="section" id="section">section</div> + <div role="sectionhead" id="sectionhead">sectionhead</div> + + <!-- roles transformed by ARIA roles of ancestors --> + <table role="grid"> + <tr> + <td id="implicit_gridcell">foo</td> + </tr> + </table> + + <!-- roles transformed by ARIA state attributes --> + <button aria-pressed="true" id="togglebutton"> + + <!-- take the first known mappable role --> + <div role="wiggly:worm abc123 button" id="unknown_roles">worm button</div> + + <!-- misc roles --> + <div role="note" id="note">note</div> + <div role="scrollbar" id="scrollbar">scrollbar</div> + + <div id="dir" role="directory"> + <div role="listitem">A</div> + <div role="listitem">B</div> + <div role="listitem">C</div> + </div> + + <p>Image: + <img id="img_eq" role="math" src="foo" alt="x^2 + y^2 + z^2"> + </p> + + <p>Text: + <span id="txt_eq" role="math" title="x^2 + y^2 + z^2">x<sup>2</sup> + + y<sup>2</sup> + z<sup>2</sup></span> + </p> + + <iframe id="iframe_aria_application" + src="data:text/html,<body role='application'>"></iframe> + <iframe id="iframe_aria_dialog" + src="data:text/html,<body role='dialog'>"></iframe> + <iframe id="iframe_aria_document" + src="data:text/html,<body role='document'>"></iframe> + <iframe id="iframe_aria_button" + src="data:text/html,<body role='button'>"></iframe> + <iframe id="iframe_aria_main" + src="data:text/html,<body role='main'>"></iframe> + <iframe id="iframe_aria_alert" + src="data:text/html,<body role='alert'>"></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/role/test_aria.xhtml b/accessible/tests/mochitest/role/test_aria.xhtml new file mode 100644 index 0000000000..9aea4ec222 --- /dev/null +++ b/accessible/tests/mochitest/role/test_aria.xhtml @@ -0,0 +1,65 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessibility Name Calculating Test."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + function doTest() + { + ok(!isAccessible("presentation_label"), + "Presentation label shouldn't be accessible."); + ok(!isAccessible("presentation_descr"), + "Presentation description shouldn't be accessible."); + + // aria-pressed + testRole("pressed_button", ROLE_TOGGLE_BUTTON); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=494345" + title="Do not create accessibles for XUL label or description having a role of 'presentation'"> + Mozilla Bug 494345 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1033283" + title="Expose pressed state on XUL menu toggle buttons"> + Mozilla Bug 1033283 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <label id="presentation_label" role="presentation" value="label"/> + <description id="presentation_descr" role="presentation" value="description"/> + <button id="pressed_button" aria-pressed="true" label="I am pressed" /> + </vbox> + + + </hbox> +</window> + diff --git a/accessible/tests/mochitest/role/test_dpub_aria.html b/accessible/tests/mochitest/role/test_dpub_aria.html new file mode 100644 index 0000000000..621c86a59b --- /dev/null +++ b/accessible/tests/mochitest/role/test_dpub_aria.html @@ -0,0 +1,114 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test DPub ARIA roles</title> + + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + + function doTest() { + // DPub ARIA role map. + testRole("doc-abstract", ROLE_SECTION); + testRole("doc-acknowledgments", ROLE_LANDMARK); + testRole("doc-afterword", ROLE_LANDMARK); + testRole("doc-appendix", ROLE_LANDMARK); + testRole("doc-backlink", ROLE_LINK); + testRole("doc-biblioentry", ROLE_LISTITEM); + testRole("doc-bibliography", ROLE_LANDMARK); + testRole("doc-biblioref", ROLE_LINK); + testRole("doc-chapter", ROLE_LANDMARK); + testRole("doc-colophon", ROLE_SECTION); + testRole("doc-conclusion", ROLE_LANDMARK); + testRole("doc-cover", ROLE_GRAPHIC); + testRole("doc-credit", ROLE_SECTION); + testRole("doc-credits", ROLE_LANDMARK); + testRole("doc-dedication", ROLE_SECTION); + testRole("doc-endnote", ROLE_LISTITEM); + testRole("doc-endnotes", ROLE_LANDMARK); + testRole("doc-epigraph", ROLE_SECTION); + testRole("doc-epilogue", ROLE_LANDMARK); + testRole("doc-errata", ROLE_LANDMARK); + testRole("doc-example", ROLE_SECTION); + testRole("doc-footnote", ROLE_FOOTNOTE); + testRole("doc-foreword", ROLE_LANDMARK); + testRole("doc-glossary", ROLE_LANDMARK); + testRole("doc-glossref", ROLE_LINK); + testRole("doc-index", ROLE_NAVIGATION); + testRole("doc-introduction", ROLE_LANDMARK); + testRole("doc-noteref", ROLE_LINK); + testRole("doc-notice", ROLE_NOTE); + testRole("doc-pagebreak", ROLE_SEPARATOR); + testRole("doc-pagelist", ROLE_NAVIGATION); + testRole("doc-part", ROLE_LANDMARK); + testRole("doc-preface", ROLE_LANDMARK); + testRole("doc-prologue", ROLE_LANDMARK); + testRole("doc-pullquote", ROLE_SECTION); + testRole("doc-qna", ROLE_SECTION); + testRole("doc-subtitle", ROLE_HEADING); + testRole("doc-tip", ROLE_NOTE); + testRole("doc-toc", ROLE_NAVIGATION); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1343537" + title="implement ARIA DPUB extension"> + Bug 1343537 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + <div id="doc-abstract" role="doc-abstract">abstract</div> + <div id="doc-acknowledgments" role="doc-acknowledgments">acknowledgments</div> + <div id="doc-afterword" role="doc-afterword">afterword</div> + <div id="doc-appendix" role="doc-appendix">appendix</div> + <div id="doc-backlink" role="doc-backlink">backlink</div> + <div id="doc-biblioentry" role="doc-biblioentry">biblioentry</div> + <div id="doc-bibliography" role="doc-bibliography">bibliography</div> + <div id="doc-biblioref" role="doc-biblioref">biblioref</div> + <div id="doc-chapter" role="doc-chapter">chapter</div> + <div id="doc-colophon" role="doc-colophon">colophon</div> + <div id="doc-conclusion" role="doc-conclusion">conclusion</div> + <div id="doc-cover" role="doc-cover">cover</div> + <div id="doc-credit" role="doc-credit">credit</div> + <div id="doc-credits" role="doc-credits">credits</div> + <div id="doc-dedication" role="doc-dedication">dedication</div> + <div id="doc-endnote" role="doc-endnote">endnote</div> + <div id="doc-endnotes" role="doc-endnotes">endnotes</div> + <div id="doc-epigraph" role="doc-epigraph">epigraph</div> + <div id="doc-epilogue" role="doc-epilogue">epilogue</div> + <div id="doc-errata" role="doc-errata">errata</div> + <div id="doc-example" role="doc-example">example</div> + <div id="doc-footnote" role="doc-footnote">footnote</div> + <div id="doc-foreword" role="doc-foreword">foreword</div> + <div id="doc-glossary" role="doc-glossary">glossary</div> + <div id="doc-glossref" role="doc-glossref">glossref</div> + <div id="doc-index" role="doc-index">index</div> + <div id="doc-introduction" role="doc-introduction">introduction</div> + <div id="doc-noteref" role="doc-noteref">noteref</div> + <div id="doc-notice" role="doc-notice">notice</div> + <div id="doc-pagebreak" role="doc-pagebreak">pagebreak</div> + <div id="doc-pagelist" role="doc-pagelist">pagelist</div> + <div id="doc-part" role="doc-part">part</div> + <div id="doc-preface" role="doc-preface">preface</div> + <div id="doc-prologue" role="doc-prologue">prologue</div> + <div id="doc-pullquote" role="doc-pullquote">pullquote</div> + <div id="doc-qna" role="doc-qna">qna</div> + <div id="doc-subtitle" role="doc-subtitle">subtitle</div> + <div id="doc-tip" role="doc-tip">tip</div> + <div id="doc-toc" role="doc-toc">toc</div> +</body> +</html> diff --git a/accessible/tests/mochitest/role/test_general.html b/accessible/tests/mochitest/role/test_general.html new file mode 100644 index 0000000000..282690cf98 --- /dev/null +++ b/accessible/tests/mochitest/role/test_general.html @@ -0,0 +1,201 @@ +<!DOCTYPE html> +<html> +<head> + <title>test nsHyperTextAccessible accesible objects creation and their roles</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + function doTests() { + // landmark tests section + testRole("frm", ROLE_FORM); + + // nsHyperTextAcc tests section + // Test html:form. + testRole("nav", ROLE_LANDMARK); + testRole("header", ROLE_LANDMARK); + testRole("footer", ROLE_LANDMARK); + testRole("article", ROLE_ARTICLE); + testRole("aside", ROLE_LANDMARK); + testRole("section", ROLE_SECTION); + + // Bug 996821 + // Check that landmark elements get accessibles with styled overflow. + testRole("section_overflow", ROLE_SECTION); + testRole("nav_overflow", ROLE_LANDMARK); + testRole("header_overflow", ROLE_SECTION); + testRole("aside_overflow", ROLE_LANDMARK); + testRole("footer_overflow", ROLE_SECTION); + testRole("article_overflow", ROLE_ARTICLE); + + // test html:div + testRole("sec", ROLE_SECTION); + + // Test html:blockquote + testRole("quote", ROLE_BLOCKQUOTE); + + // Test html:h, all levels + testRole("head1", ROLE_HEADING); + testRole("head2", ROLE_HEADING); + testRole("head3", ROLE_HEADING); + testRole("head4", ROLE_HEADING); + testRole("head5", ROLE_HEADING); + testRole("head6", ROLE_HEADING); + + // Test that an html:input @type="file" is exposed as ROLE_GROUPING. + testRole("data", ROLE_GROUPING); + + // Test that input type="checkbox" and type="radio" are + // exposed as such regardless of appearance style. + testRole("checkbox_regular", ROLE_CHECKBUTTON); + testRole("checkbox_appearance_none", ROLE_CHECKBUTTON); + testRole("radio_regular", ROLE_RADIOBUTTON); + testRole("radio_appearance_none", ROLE_RADIOBUTTON); + + // Test regular paragraph by comparison to make sure exposure does not + // get broken. + testRole("p", ROLE_PARAGRAPH); + + // Test dl, dt, dd + testRole("definitionlist", ROLE_DEFINITION_LIST); + testRole("definitionterm", ROLE_TERM); + testRole("definitiondescription", ROLE_DEFINITION); + + // Has click, mousedown or mouseup listeners. + testRole("span1", ROLE_TEXT); + testRole("span2", ROLE_TEXT); + testRole("span3", ROLE_TEXT); + + // Test role of listbox inside combobox + testRole("listbox1", ROLE_COMBOBOX_LIST); + testRole("listbox2", ROLE_COMBOBOX_LIST); + + // Test role of menu and li items in menu + testRole("menu", ROLE_LIST); + testRole("menuItem", ROLE_LISTITEM); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> +<body> + + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=472326" + title="html:input of type "file" no longer rendered to screen readers"> + Mozilla Bug 472326 + </a><br> + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=474261" + title="Test remaining implementations in nsHyperTextAccessible::GetRole"> + bug 474261 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=423409" + title="Expose click action if mouseup and mousedown are registered"> + Mozilla Bug 423409 + </a> + <a target="_blank" + title="Provide mappings for html5 <nav> <header> <footer> <article>" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=593368"> + Bug 593368 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=613502" + title="Map <article> like we do aria role article"> + Bug 613502 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=610650" + title="Change implementation of HTML5 landmark elements to conform"> + Bug 610650 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=614310" + title="Map section to pane (like role=region)"> + Mozilla Bug 614310 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=734982" + title="Map ARIA role FORM"> + Bug 734982 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1044431" + title="Listbox owned by combobox has the wrong role"> + Mozilla Bug 1044431 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <form id="frm" action="submit.php" method="post"> + <label for="data">File</label>: + <input type="file" id="data" name="data" size="50"/> + <input type="checkbox" id="checkbox_regular" value="Check me"/> + <input type="checkbox" style="-moz-appearance: none;" id="checkbox_appearance_none" value="Check me"/> + <input type="radio" id="radio_regular" value="Check me"/> + <input type="radio" style="-moz-appearance: none;" id="radio_appearance_none" value="Check me"/> + </form> + + <nav id="nav">a nav</nav> + <header id="header">a header</header> + <footer id="footer">a footer</footer> + <article id="article">an article</article> + <aside id="aside">by the way I am an aside</aside> + <section id="section">a section</section> + + <section style="overflow: hidden;" id="section_overflow"> + <nav style="overflow: hidden;" + id="nav_overflow">overflow nav</nav> + <header style="overflow: hidden;" + id="header_overflow">overflow header</header> + <aside style="overflow: hidden;" + id="aside_overflow">overflow aside</aside> + <footer style="overflow: hidden;" + id="footer_overflow">overflow footer</footer> + </section> + <article style="overflow: hidden;" + id="article_overflow">overflow article</article> + + <p id="p">A paragraph for comparison.</p> + <div id="sec">A normal div</div> + <blockquote id="quote">A citation</blockquote> + <h1 id="head1">A heading level 1</h1> + <h2 id="head2">A heading level 2</h2> + <h3 id="head3">A heading level 3</h3> + <h4 id="head4">A heading level 4</h4> + <h5 id="head5">A heading level 5</h5> + <h6 id="head6">A heading level 6</h6> + + <dl id="definitionlist"> + <dt id="definitionterm">gecko</dt> + <dd id="definitiondescription">geckos have sticky toes</dd> + </dl> + + <span id="span1" onclick="">clickable span</span> + <span id="span2" onmousedown="">clickable span</span> + <span id="span3" onmouseup="">clickable span</span> + + <div id="combobox1" role="combobox"> + <div id="listbox1" role="listbox"></div> + </div> + <div id="combobox2" role="combobox" aria-owns="listbox2"></div> + <div id="listbox2" role="listbox"></div> + + <menu id="menu"> + <li id="menuItem">menu item!</li> + </menu> +</body> +</html> diff --git a/accessible/tests/mochitest/role/test_general.xhtml b/accessible/tests/mochitest/role/test_general.xhtml new file mode 100644 index 0000000000..95de7775ea --- /dev/null +++ b/accessible/tests/mochitest/role/test_general.xhtml @@ -0,0 +1,59 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessibility Role XUL Test."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + <![CDATA[ + function doTest() + { + ok(!isAccessible("image"), + "image without tooltiptext shouldn't be accessible."); + testRole("image-tooltiptext", ROLE_GRAPHIC); + + ok(!isAccessible("statusbarpanel"), + "statusbarpanel shouldn't be accessible."); + testRole("statusbar", ROLE_STATUSBAR); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=900097" + title="statusbarpanel shouldn't be a button accessible"> + Mozilla Bug 900097 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <image id="image" src="../moz.png"/> + <image id="image-tooltiptext" src="../moz.png" tooltiptext="hello"/> + + <statusbarpanel id="statusbarpanel"></statusbarpanel> + <statusbar id="statusbar"></statusbar> + + </hbox> +</window> + diff --git a/accessible/tests/mochitest/role/test_graphics_aria.html b/accessible/tests/mochitest/role/test_graphics_aria.html new file mode 100644 index 0000000000..8ddfe9224d --- /dev/null +++ b/accessible/tests/mochitest/role/test_graphics_aria.html @@ -0,0 +1,42 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test Graphics ARIA roles</title> + + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + + function doTest() { + // Graphics ARIA role map. + testRole("graphics-document", ROLE_NON_NATIVE_DOCUMENT); + testRole("graphics-object", ROLE_GROUPING); + testRole("graphics-symbol", ROLE_GRAPHIC); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1432513" + title="implement ARIA Graphics roles"> + Bug 1432513 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"></pre> + <div id="graphics-document" role="graphics-document">document</div> + <div id="graphics-object" role="graphics-object">object</div> + <div id="graphics-symbol" role="graphics-symbol">symbol</div> +</body> +</html> diff --git a/accessible/tests/mochitest/role/test_svg.html b/accessible/tests/mochitest/role/test_svg.html new file mode 100644 index 0000000000..77444e8187 --- /dev/null +++ b/accessible/tests/mochitest/role/test_svg.html @@ -0,0 +1,88 @@ +<!DOCTYPE html> +<html> +<head> + <title>SVG elements accessible roles</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + function doTests() { + testRole("svg", ROLE_DIAGRAM); + testRole("g", ROLE_GROUPING); + testRole("rect", ROLE_GRAPHIC); + testRole("circle", ROLE_GRAPHIC); + testRole("ellipse", ROLE_GRAPHIC); + testRole("line", ROLE_GRAPHIC); + testRole("polygon", ROLE_GRAPHIC); + testRole("polyline", ROLE_GRAPHIC); + testRole("path", ROLE_GRAPHIC); + testRole("image", ROLE_GRAPHIC); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=822983" + title="Map SVG graphic elements to accessibility API"> + Bug 822983 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <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> + +</body> +</html> diff --git a/accessible/tests/mochitest/scroll/a11y.ini b/accessible/tests/mochitest/scroll/a11y.ini new file mode 100644 index 0000000000..3f58374c93 --- /dev/null +++ b/accessible/tests/mochitest/scroll/a11y.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_zoom.html] diff --git a/accessible/tests/mochitest/scroll/test_zoom.html b/accessible/tests/mochitest/scroll/test_zoom.html new file mode 100644 index 0000000000..b0ece28a91 --- /dev/null +++ b/accessible/tests/mochitest/scroll/test_zoom.html @@ -0,0 +1,145 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test scrollToPoint when page is zoomed</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + + <script type="application/javascript"> + function testScrollToPoint() { + // scrollToPoint relative screen + var anchor = getAccessible("bottom1"); + let [x /* y */] = getPos(anchor); + var [docX, docY] = getPos(document); + + anchor.scrollToPoint(COORDTYPE_SCREEN_RELATIVE, docX, docY); + testPos(anchor, [x, docY]); + + // scrollToPoint relative window + anchor = getAccessible("bottom2"); + [x /* y */] = getPos(anchor); + var wnd = getRootAccessible().DOMDocument.defaultView; + var [screenX, screenY] = CSSToDevicePixels(wnd, wnd.screenX, wnd.screenY); + let scrollToX = docX - screenX, scrollToY = docY - screenY; + + anchor.scrollToPoint(COORDTYPE_WINDOW_RELATIVE, scrollToX, scrollToY); + testPos(anchor, [x, docY]); + + // scrollToPoint relative parent + anchor = getAccessible("bottom3"); + [x /* y */] = getPos(anchor); + var [parentX, parentY] = getPos(anchor.parent); + scrollToX = parentX - docX; + scrollToY = parentY - docY; + + anchor.scrollToPoint(COORDTYPE_PARENT_RELATIVE, scrollToX, scrollToY); + testPos(anchor, [x, docY]); + } + + function doTest() { + testScrollToPoint(); + zoomDocument(document, 2.0); + testScrollToPoint(); // zoom and test again + + zoomDocument(document, 1.0); + SimpleTest.finish(); + } + + addA11yLoadEvent(doTest); + SimpleTest.waitForExplicitFinish(); + </script> + +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=727942" + title="scrollToPoint is broken when page is zoomed"> + Mozilla Bug 727942 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <h1>Below there is a bunch of named anchors</h1> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + This is in the middle anchor #1<a id="bottom1"></a> + <br><br><br><br><br><br><br><br><br><br> + This is in the middle anchor #2<a id="bottom2"></a> + <br><br><br><br><br><br><br><br><br><br> + This is in the middle anchor #3<a id="bottom3"></a> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> + <br><br><br><br><br><br><br><br><br><br> +</body> +</html> diff --git a/accessible/tests/mochitest/selectable.js b/accessible/tests/mochitest/selectable.js new file mode 100644 index 0000000000..e71fca489b --- /dev/null +++ b/accessible/tests/mochitest/selectable.js @@ -0,0 +1,123 @@ +/* import-globals-from common.js */ +/* import-globals-from states.js */ + +/** + * Test selection getter methods of nsIAccessibleSelectable. + * + * @param aIdentifier [in] selectable container accessible + * @param aSelectedChildren [in] array of selected children + */ +function testSelectableSelection(aIdentifier, aSelectedChildren, aMsg) { + var acc = getAccessible(aIdentifier, [nsIAccessibleSelectable]); + if (!acc) { + return; + } + + var msg = aMsg ? aMsg : ""; + var len = aSelectedChildren.length; + + // getSelectedChildren + var selectedChildren = acc.selectedItems; + is( + selectedChildren ? selectedChildren.length : 0, + len, + msg + + "getSelectedChildren: wrong selected children count for " + + prettyName(aIdentifier) + ); + + for (let idx = 0; idx < len; idx++) { + let expectedAcc = getAccessible(aSelectedChildren[idx]); + var actualAcc = selectedChildren.queryElementAt(idx, nsIAccessible); + is( + actualAcc, + expectedAcc, + msg + + "getSelectedChildren: wrong selected child at index " + + idx + + " for " + + prettyName(aIdentifier) + + " { actual : " + + prettyName(actualAcc) + + ", expected: " + + prettyName(expectedAcc) + + "}" + ); + } + + // selectedItemCount + is( + acc.selectedItemCount, + aSelectedChildren.length, + "selectedItemCount: wrong selected children count for " + + prettyName(aIdentifier) + ); + + // getSelectedItemAt + for (let idx = 0; idx < len; idx++) { + let expectedAcc = getAccessible(aSelectedChildren[idx]); + is( + acc.getSelectedItemAt(idx), + expectedAcc, + msg + + "getSelectedItemAt: wrong selected child at index " + + idx + + " for " + + prettyName(aIdentifier) + ); + } + + // isItemSelected + testIsItemSelected(acc, acc, { value: 0 }, aSelectedChildren, msg); +} + +/** + * Test isItemSelected method, helper for testSelectableSelection + */ +function testIsItemSelected( + aSelectAcc, + aTraversedAcc, + aIndexObj, + aSelectedChildren, + aMsg +) { + var childCount = aTraversedAcc.childCount; + for (var idx = 0; idx < childCount; idx++) { + var child = aTraversedAcc.getChildAt(idx); + var [state /* extraState */] = getStates(child); + if (state & STATE_SELECTABLE) { + var isSelected = false; + var len = aSelectedChildren.length; + for (var jdx = 0; jdx < len; jdx++) { + if (child == getAccessible(aSelectedChildren[jdx])) { + isSelected = true; + break; + } + } + + // isItemSelected + is( + aSelectAcc.isItemSelected(aIndexObj.value++), + isSelected, + aMsg + + "isItemSelected: wrong selected child " + + prettyName(child) + + " for " + + prettyName(aSelectAcc) + ); + + // selected state + testStates( + child, + isSelected ? STATE_SELECTED : 0, + 0, + !isSelected ? STATE_SELECTED : 0, + 0 + ); + + continue; + } + + testIsItemSelected(aSelectAcc, child, aIndexObj, aSelectedChildren); + } +} diff --git a/accessible/tests/mochitest/selectable/a11y.ini b/accessible/tests/mochitest/selectable/a11y.ini new file mode 100644 index 0000000000..8f109e8b81 --- /dev/null +++ b/accessible/tests/mochitest/selectable/a11y.ini @@ -0,0 +1,12 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/treeview.css + +[test_aria.html] +[test_listbox.xhtml] +[test_menu.xhtml] +[test_menulist.xhtml] +[test_select.html] +[test_tabs.xhtml] +[test_tree.xhtml] diff --git a/accessible/tests/mochitest/selectable/test_aria.html b/accessible/tests/mochitest/selectable/test_aria.html new file mode 100644 index 0000000000..46ecd94031 --- /dev/null +++ b/accessible/tests/mochitest/selectable/test_aria.html @@ -0,0 +1,246 @@ +<html> + +<head> + <title>nsIAccessibleSelectable ARIA widgets testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../selectable.js"></script> + + <script type="application/javascript"> + function testSelectable(aID, aSelectableChildren) { + var acc = getAccessible(aID, [nsIAccessibleSelectable]); + + testSelectableSelection(acc, []); + + acc.selectAll(); + testSelectableSelection(acc, aSelectableChildren); + + acc.unselectAll(); + testSelectableSelection(acc, []); + } + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // role="tablist" + + let id = "tablist_single"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + testSelectableSelection(id, [ ]); + + // //////////////////////////////////////////////////////////////////////// + // role="tablist" aria-multiselectable + + id = "tablist_multi"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + testSelectableSelection(id, [ ]); + + let select = getAccessible(id, [nsIAccessibleSelectable]); + select.addItemToSelection(0); + testSelectableSelection(id, [ "tab_multi1" ]); + select.removeItemFromSelection(0); + testSelectableSelection(id, [ ]); + select.selectAll(); + testSelectableSelection(id, [ "tab_multi1", "tab_multi2" ]); + select.unselectAll(); + testSelectableSelection(id, [ ]); + + // //////////////////////////////////////////////////////////////////////// + // role="listbox" + + id = "listbox1"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + testSelectableSelection(id, [ ]); + + // //////////////////////////////////////////////////////////////////////// + // role="listbox" aria-multiselectable + + id = "listbox2"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + testSelectableSelection(id, [ ]); + + select = getAccessible(id, [nsIAccessibleSelectable]); + select.addItemToSelection(0); + testSelectableSelection(id, [ "listbox2_item1" ]); + select.removeItemFromSelection(0); + testSelectableSelection(id, [ ]); + select.selectAll(); + testSelectableSelection(id, [ "listbox2_item1", "listbox2_item2" ]); + select.unselectAll(); + testSelectableSelection(id, [ ]); + + // //////////////////////////////////////////////////////////////////////// + // role="grid" + + id = "grid1"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + testSelectableSelection(id, [ ]); + + // //////////////////////////////////////////////////////////////////////// + // role="tree" + + id = "tree1"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + testSelectableSelection(id, [ ]); + + // //////////////////////////////////////////////////////////////////////// + // role="treegrid" + + id = "treegrid1"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + testSelectableSelection(id, [ ]); + + // //////////////////////////////////////////////////////////////////////// + // role="grid" aria-multiselectable, selectable children in subtree + + id = "grid2"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + testSelectable(id, + ["grid2_colhead1", "grid2_colhead2", "grid2_colhead3", + "grid2_rowhead", "grid2_cell1", "grid2_cell2"]); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=530014" + title="ARIA single selectable widget should implement nsIAccessibleSelectable"> + Mozilla Bug 530014 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=566551" + title="ARIA grid and accessible selectable methods shouldn't use GetNextSibling"> + Mozilla Bug 566551 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=590176" + title="add pseudo SelectAccessible interface"> + Mozilla Bug 590176 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=804040" + title="Selection event not fired when selection of ARIA tab changes"> + Mozilla Bug 804040 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="tablist" id="tablist_single"> + <div role="tab">tab1</div> + <div role="tab">tab2</div> + </div> + + <div role="tablist" id="tablist_multi" aria-multiselectable="true"> + <div role="tab" id="tab_multi1">tab1</div> + <div role="tab" id="tab_multi2">tab2</div> + </div> + + <div role="listbox" id="listbox1"> + <div role="option">item1</div> + <div role="option">item2</div> + </div> + + <div role="listbox" id="listbox2" aria-multiselectable="true"> + <div role="option" id="listbox2_item1">item1</div> + <div role="option" id="listbox2_item2">item2</div> + </div> + + <div role="grid" id="grid1"> + <div role="row"> + <span role="gridcell">cell</span> + <span role="gridcell">cell</span> + </div> + <div role="row"> + <span role="gridcell">cell</span> + <span role="gridcell">cell</span> + </div> + </div> + + <div role="tree" id="tree1"> + <div role="treeitem"> + item1 + <div role="group"> + <div role="treeitem">item1.1</div> + </div> + </div> + <div>item2</div> + </div> + + <div role="treegrid" id="treegrid1"> + <div role="row" aria-level="1"> + <span role="gridcell">cell</span> + <span role="gridcell">cell</span> + </div> + <div role="row" aria-level="2"> + <span role="gridcell">cell</span> + <span role="gridcell">cell</span> + </div> + <div role="row" aria-level="1"> + <span role="gridcell">cell</span> + <span role="gridcell">cell</span> + </div> + </div> + + <table tabindex="0" border="2" cellspacing="0" id="grid2" role="grid" + aria-multiselectable="true"> + <thead> + <tr> + <th tabindex="-1" role="columnheader" id="grid2_colhead1" + style="width:6em">Entry #</th> + <th tabindex="-1" role="columnheader" id="grid2_colhead2" + style="width:10em">Date</th> + <th tabindex="-1" role="columnheader" id="grid2_colhead3" + style="width:20em">Expense</th> + </tr> + </thead> + <tbody> + <tr> + <td tabindex="-1" role="rowheader" id="grid2_rowhead" + aria-readonly="true">1</td> + <td tabindex="-1" role="gridcell" id="grid2_cell1" + aria-selected="false">03/14/05</td> + <td tabindex="-1" role="gridcell" id="grid2_cell2" + aria-selected="false">Conference Fee</td> + </tr> + </tobdy> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/selectable/test_listbox.xhtml b/accessible/tests/mochitest/selectable/test_listbox.xhtml new file mode 100644 index 0000000000..b915b2790f --- /dev/null +++ b/accessible/tests/mochitest/selectable/test_listbox.xhtml @@ -0,0 +1,144 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<?xml-stylesheet href="../treeview.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL tree selectable tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../selectable.js" /> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Test + + //gA11yEventDumpID = "debug"; + + var gQueue = null; + + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // single selectable listbox, the first item is selected by default + + var id = "listbox"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for list of " + id); + + var select = getAccessible(id, [nsIAccessibleSelectable]); + select.removeItemFromSelection(0); + testSelectableSelection(select, [ ]); + + select.addItemToSelection(1); + testSelectableSelection(select, [ "lb1_item2" ], "addItemToSelect(1): "); + + select.removeItemFromSelection(1); + testSelectableSelection(select, [ ], + "removeItemFromSelection(1): "); + + todo(!select.selectAll(), + "No way to select all items in listbox '" + id + "'"); + testSelectableSelection(select, [ "lb1_item1" ], "selectAll: "); + + select.addItemToSelection(1); + select.unselectAll(); + testSelectableSelection(select, [ ], "unselectAll: "); + + ////////////////////////////////////////////////////////////////////////// + // multiple selectable listbox + + id = "listbox2"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for list of " + id); + + select = getAccessible(id, [nsIAccessibleSelectable]); + testSelectableSelection(select, [ ]); + + select.addItemToSelection(1); + testSelectableSelection(select, [ "lb2_item2" ], "addItemToSelect(1): "); + + select.removeItemFromSelection(1); + testSelectableSelection(select, [ ], + "removeItemFromSelection(1): "); + + is(select.selectAll(), true, + "All items should be selected in listbox '" + id + "'"); + testSelectableSelection(select, [ "lb2_item1", "lb2_item2" ], + "selectAll: "); + + select.unselectAll(); + testSelectableSelection(select, [ ], "unselectAll: "); + + ////////////////////////////////////////////////////////////////////////// + // listbox with headers + + // XXX: addItemToSelection/removeItemFromSelection don't work correctly + // on listboxes with headers because header is inserted into hierarchy + // and child indexes that are used in these methods are shifted (see bug + // 591939). + todo(false, + "Fix addItemToSelection/removeItemFromSelection on listboxes with headers."); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=590176" + title="add pseudo SelectAccessible interface"> + Mozilla Bug 590176 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <richlistbox id="listbox"> + <richlistitem id="lb1_item1"> + <label value="cell0"/> + <label value="cell1"/> + </richlistitem> + <richlistitem id="lb1_item2"> + <label value="cell3"/> + <label value="cell4"/> + </richlistitem> + </richlistbox> + + <richlistbox id="listbox2" seltype="multiple"> + <richlistitem id="lb2_item1"> + <label value="cell0"/> + <label value="cell1"/> + </richlistitem> + <richlistitem id="lb2_item2"> + <label value="cell3"/> + <label value="cell4"/> + </richlistitem> + </richlistbox> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/selectable/test_menu.xhtml b/accessible/tests/mochitest/selectable/test_menu.xhtml new file mode 100644 index 0000000000..bfef622348 --- /dev/null +++ b/accessible/tests/mochitest/selectable/test_menu.xhtml @@ -0,0 +1,77 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<?xml-stylesheet href="../treeview.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL tree selectable tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../selectable.js" /> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Test + + //gA11yEventDumpID = "debug"; + + var gQueue = null; + + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // menu + + var id = "menu"; + var menu = getAccessible("menu"); + var menuList = menu.firstChild; + todo(isAccessible(menuList, [nsIAccessibleSelectable]), + "No selectable accessible for list of menu '" + id + "'"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=590176" + title="add pseudo SelectAccessible interface"> + Mozilla Bug 590176 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menu label="menu" id="menu"> + <menupopup> + <menuitem label="item1" id="m_item1"/> + <menuitem label="item2" id="m_item2"/> + </menupopup> + </menu> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/selectable/test_menulist.xhtml b/accessible/tests/mochitest/selectable/test_menulist.xhtml new file mode 100644 index 0000000000..2e4b5ac08f --- /dev/null +++ b/accessible/tests/mochitest/selectable/test_menulist.xhtml @@ -0,0 +1,95 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<?xml-stylesheet href="../treeview.css" type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL tree selectable tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../selectable.js" /> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Test + + //gA11yEventDumpID = "debug"; + + var gQueue = null; + + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // menulist aka combobox + + var id = "combobox"; + var combobox = getAccessible(id); + var comboboxList = combobox.firstChild; + ok(isAccessible(comboboxList, [nsIAccessibleSelectable]), + "No selectable accessible for list of " + id); + + var select = getAccessible(comboboxList, [nsIAccessibleSelectable]); + testSelectableSelection(select, [ "cb1_item1" ]); + + select.addItemToSelection(1); + testSelectableSelection(select, [ "cb1_item2" ], "addItemToSelection(1): "); + + select.removeItemFromSelection(1); + testSelectableSelection(select, [ ], + "removeItemFromSelection(1): "); + + is(select.selectAll(), false, + "No way to select all items in combobox '" + id + "'"); + testSelectableSelection(select, [ ], "selectAll: "); + + select.addItemToSelection(1); + select.unselectAll(); + testSelectableSelection(select, [ ], "unselectAll: "); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=590176" + title="add pseudo SelectAccessible interface"> + Mozilla Bug 590176 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menulist id="combobox"> + <menupopup> + <menuitem id="cb1_item1" label="item1"/> + <menuitem id="cb1_item2" label="item2"/> + </menupopup> + </menulist> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/selectable/test_select.html b/accessible/tests/mochitest/selectable/test_select.html new file mode 100644 index 0000000000..324831a498 --- /dev/null +++ b/accessible/tests/mochitest/selectable/test_select.html @@ -0,0 +1,241 @@ +<html> + +<head> + <title>nsIAccessibleSelectable HTML select testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../selectable.js"></script> + + <script type="application/javascript"> + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // select@size="1" aka combobox + + var id = "combobox"; + var combobox = getAccessible(id); + var comboboxList = combobox.firstChild; + ok(isAccessible(comboboxList, [nsIAccessibleSelectable]), + "No selectable accessible for list of " + id); + + var select = getAccessible(comboboxList, [nsIAccessibleSelectable]); + testSelectableSelection(select, [ "cb1_item1" ]); + + // select 2nd item + select.addItemToSelection(1); + testSelectableSelection(select, [ "cb1_item2" ], "addItemToSelection(1): "); + + // unselect 2nd item, 1st item gets selected automatically + select.removeItemFromSelection(1); + testSelectableSelection(select, [ "cb1_item1" ], + "removeItemFromSelection(1): "); + + // doesn't change selection + is(select.selectAll(), false, + "No way to select all items in combobox '" + id + "'"); + testSelectableSelection(select, [ "cb1_item1" ], "selectAll: "); + + // doesn't change selection + select.unselectAll(); + testSelectableSelection(select, [ "cb1_item1" ], "unselectAll: "); + + // //////////////////////////////////////////////////////////////////////// + // select@size="1" with optgroups + + id = "combobox2"; + combobox = getAccessible(id); + comboboxList = combobox.firstChild; + ok(isAccessible(comboboxList, [nsIAccessibleSelectable]), + "No selectable accessible for list of " + id); + + select = getAccessible(comboboxList, [nsIAccessibleSelectable]); + testSelectableSelection(select, [ "cb2_item1" ]); + + select.addItemToSelection(1); + testSelectableSelection(select, [ "cb2_item2" ]); + + select.removeItemFromSelection(1); + testSelectableSelection(select, [ "cb2_item1" ]); + + is(select.selectAll(), false, + "No way to select all items in combobox " + id + "'"); + testSelectableSelection(select, [ "cb2_item1" ]); + + select.unselectAll(); + testSelectableSelection(select, [ "cb2_item1" ]); + + // //////////////////////////////////////////////////////////////////////// + // select@size="4" aka single selectable listbox + + id = "listbox"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + select = getAccessible(id, [nsIAccessibleSelectable]); + testSelectableSelection(select, [ ]); + + // select 2nd item + select.addItemToSelection(1); + testSelectableSelection(select, [ "lb1_item2" ], "addItemToSelection(1): "); + + // unselect 2nd item, 1st item gets selected automatically + select.removeItemFromSelection(1); + testSelectableSelection(select, [ ], + "removeItemFromSelection(1): "); + + // doesn't change selection + is(select.selectAll(), false, + "No way to select all items in single selectable listbox '" + id + "'"); + testSelectableSelection(select, [ ], "selectAll: "); + + // doesn't change selection + select.unselectAll(); + testSelectableSelection(select, [ ], "unselectAll: "); + + // //////////////////////////////////////////////////////////////////////// + // select@size="4" with optgroups, single selectable + + id = "listbox2"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + select = getAccessible(id, [nsIAccessibleSelectable]); + testSelectableSelection(select, [ ]); + + select.addItemToSelection(1); + testSelectableSelection(select, [ "lb2_item2" ]); + + select.removeItemFromSelection(1); + testSelectableSelection(select, [ ]); + + is(select.selectAll(), false, + "No way to select all items in single selectable listbox " + id + "'"); + testSelectableSelection(select, [ ]); + + select.unselectAll(); + testSelectableSelection(select, [ ]); + + // //////////////////////////////////////////////////////////////////////// + // select@size="4" multiselect aka listbox + + id = "listbox3"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + select = getAccessible(id, [nsIAccessibleSelectable]); + testSelectableSelection(select, [ ]); + + select.addItemToSelection(0); + testSelectableSelection(select, [ "lb3_item1" ], "addItemToSelection: "); + + select.removeItemFromSelection(0); + testSelectableSelection(select, [ ], "removeItemFromSelection: "); + + is(select.selectAll(), true, + "All items in listbox '" + id + "' should be selected"); + testSelectableSelection(select, [ "lb3_item1", "lb3_item2"], + "selectAll: "); + + select.unselectAll(); + testSelectableSelection(select, [ ], "unselectAll: "); + + // //////////////////////////////////////////////////////////////////////// + // select@size="4" multiselect with optgroups + + id = "listbox4"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for " + id); + + select = getAccessible(id, [nsIAccessibleSelectable]); + testSelectableSelection(select, [ ]); + + select.addItemToSelection(0); + testSelectableSelection(select, [ "lb4_item1" ]); + + select.removeItemFromSelection(0); + testSelectableSelection(select, [ ]); + + is(select.selectAll(), true, + "All items in listbox '" + id + "' should be selected"); + testSelectableSelection(select, [ "lb4_item1", "lb4_item2"]); + + select.unselectAll(); + testSelectableSelection(select, [ ]); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=530014" + title="ARIA single selectable widget should implement nsIAccessibleSelectable"> + Mozilla Bug 530014 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=590176" + title="add pseudo SelectAccessible interface"> + Mozilla Bug 590176 + </a><br> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="combobox"> + <option id="cb1_item1">option1</option> + <option id="cb1_item2">option2</option> + </select> + + <select id="combobox2"> + <option id="cb2_item1">option1</option> + <optgroup>optgroup + <option id="cb2_item2">option2</option> + </optgroup> + </select> + + <select id="listbox" size="4"> + <option id="lb1_item1">option1</option> + <option id="lb1_item2">option2</option> + </select> + + <select id="listbox2" size="4"> + <option id="lb2_item1">option1</option> + <optgroup>optgroup> + <option id="lb2_item2">option2</option> + </optgroup> + </select> + + <select id="listbox3" size="4" multiple="true"> + <option id="lb3_item1">option1</option> + <option id="lb3_item2">option2</option> + </select> + + <select id="listbox4" size="4" multiple="true"> + <option id="lb4_item1">option1</option> + <optgroup>optgroup> + <option id="lb4_item2">option2</option> + </optgroup> + </select> + +</body> +</html> diff --git a/accessible/tests/mochitest/selectable/test_tabs.xhtml b/accessible/tests/mochitest/selectable/test_tabs.xhtml new file mode 100644 index 0000000000..bc16ab0fe7 --- /dev/null +++ b/accessible/tests/mochitest/selectable/test_tabs.xhtml @@ -0,0 +1,93 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL tabs selectable tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../selectable.js" /> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + var id = "tabs_single"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for tabs_single"); + var select = getAccessible(id, [nsIAccessibleSelectable]); + + testSelectableSelection(select, ["tab_single1"]); + + select.unselectAll(); + select.addItemToSelection(1); // tab_single2 + testSelectableSelection(select, ["tab_single2"], "select tab_single2: "); + + id = "tabs_multi"; + ok(isAccessible(id, [nsIAccessibleSelectable]), + "No selectable accessible for tabs_multi"); + select = getAccessible(id, [nsIAccessibleSelectable]); + + // Make sure both XUL selection and ARIA selection are included. + testSelectableSelection(select, ["tab_multi_xul1", "tab_multi_aria"]); + + select.unselectAll(); + select.addItemToSelection(2); // tab_multi_xul2 + // We can only affect XUL selection, so ARIA selection won't change. + testSelectableSelection(select, ["tab_multi_aria", "tab_multi_xul2"], + "select tab_multi_xul2: "); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1480058" + title="XUL tabs don't support ARIA selection"> + Mozilla Bug 1480058 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tabbox> + <tabs id="tabs_single"> + <tab id="tab_single1" label="tab1" selected="true"/> + <tab id="tab_single2" label="tab2"/> + </tabs> + </tabbox> + + <tabbox> + <tabs id="tabs_multi" aria-multiselectable="true"> + <tab id="tab_multi_xul1" label="tab1" selected="true"/> + <tab id="tab_multi_aria" label="tab2" aria-selected="true"/> + <tab id="tab_multi_xul2" label="tab3"/> + </tabs> + </tabbox> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/selectable/test_tree.xhtml b/accessible/tests/mochitest/selectable/test_tree.xhtml new file mode 100644 index 0000000000..5527625b24 --- /dev/null +++ b/accessible/tests/mochitest/selectable/test_tree.xhtml @@ -0,0 +1,171 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<?xml-stylesheet href="../treeview.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL tree selectable tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../selectable.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; + + /** + * Event queue invoker object to test accessible states for XUL tree + * accessible. + */ + function statesChecker(aTreeID, aView) + { + this.DOMNode = getNode(aTreeID); + + this.invoke = function invoke() + { + this.DOMNode.view = aView; + } + this.check = function check() + { + var tree = getAccessible(this.DOMNode); + + var isTreeMultiSelectable = false; + var seltype = this.DOMNode.getAttribute("seltype"); + if (seltype != "single") + isTreeMultiSelectable = true; + + // selectAll + var accSelectable = getAccessible(this.DOMNode, + [nsIAccessibleSelectable]); + ok(accSelectable, "tree is not selectable!"); + if (accSelectable) { + is(accSelectable.selectAll(), isTreeMultiSelectable, + "SelectAll is not correct for seltype: " + seltype); + } + + var selectedChildren = []; + if (isTreeMultiSelectable) { + var rows = tree.children; + for (var i = 0; i < rows.length; i++) { + var row = rows.queryElementAt(i, nsIAccessible); + if (getRole(row) == ROLE_OUTLINEITEM || getRole(row) == ROLE_ROW) + selectedChildren.push(row); + } + } + testSelectableSelection(accSelectable, selectedChildren, + "selectAll test. "); + + // unselectAll + accSelectable.unselectAll(); + testSelectableSelection(accSelectable, [], "unselectAll test. "); + + // addItemToSelection + accSelectable.addItemToSelection(1); + accSelectable.addItemToSelection(3); + + selectedChildren = isTreeMultiSelectable ? + [ accSelectable.getChildAt(2), accSelectable.getChildAt(4) ] : + [ accSelectable.getChildAt(2) ]; + testSelectableSelection(accSelectable, selectedChildren, + "addItemToSelection test. "); + + // removeItemFromSelection + accSelectable.removeItemFromSelection(1); + + selectedChildren = isTreeMultiSelectable ? + [ accSelectable.getChildAt(4) ] : [ ]; + testSelectableSelection(accSelectable, selectedChildren, + "removeItemFromSelection test. "); + } + + this.getID = function getID() + { + "tree processor for " + prettyName(aTreeID); + } + } + + var gQueue = null; + + function doTest() + { + gQueue = new eventQueue(EVENT_REORDER); + gQueue.push(new statesChecker("tree", new nsTreeTreeView())); + gQueue.push(new statesChecker("treesingle", new nsTreeTreeView())); + gQueue.push(new statesChecker("tabletree", new nsTreeTreeView())); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=523118" + title="we mistake 'cell' and text' xul tree seltypes for multiselects"> + Mozilla Bug 523118 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=624977" + title="Optimize nsXulTreeAccessible selectedItems()"> + Mozilla Bug 624977 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <tree id="treesingle" flex="1" seltype="single"> + <treecols> + <treecol id="col_single" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <tree id="tabletree" flex="1" editable="true"> + <treecols> + <treecol id="tabletree_col1" cycler="true" label="cycler"/> + <treecol id="tabletree_col2" flex="1" primary="true" label="column1"/> + <treecol id="tabletree_col3" flex="1" label="column2"/> + <treecol id="tabletree_col4" flex="1" label="checker" + type="checkbox" editable="true"/> + </treecols> + <treechildren/> + </tree> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/states.js b/accessible/tests/mochitest/states.js new file mode 100644 index 0000000000..8e394b11a1 --- /dev/null +++ b/accessible/tests/mochitest/states.js @@ -0,0 +1,363 @@ +// ////////////////////////////////////////////////////////////////////////////// +// Helper functions for accessible states testing. +// +// requires: +// common.js +// role.js +// +// ////////////////////////////////////////////////////////////////////////////// +/* import-globals-from common.js */ +/* import-globals-from role.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// State constants + +// const STATE_BUSY is defined in common.js +const STATE_CHECKED = nsIAccessibleStates.STATE_CHECKED; +const STATE_CHECKABLE = nsIAccessibleStates.STATE_CHECKABLE; +const STATE_COLLAPSED = nsIAccessibleStates.STATE_COLLAPSED; +const STATE_DEFAULT = nsIAccessibleStates.STATE_DEFAULT; +const STATE_EXPANDED = nsIAccessibleStates.STATE_EXPANDED; +const STATE_EXTSELECTABLE = nsIAccessibleStates.STATE_EXTSELECTABLE; +const STATE_FLOATING = nsIAccessibleStates.STATE_FLOATING; +const STATE_FOCUSABLE = nsIAccessibleStates.STATE_FOCUSABLE; +const STATE_FOCUSED = nsIAccessibleStates.STATE_FOCUSED; +const STATE_HASPOPUP = nsIAccessibleStates.STATE_HASPOPUP; +const STATE_INVALID = nsIAccessibleStates.STATE_INVALID; +const STATE_INVISIBLE = nsIAccessibleStates.STATE_INVISIBLE; +const STATE_LINKED = nsIAccessibleStates.STATE_LINKED; +const STATE_MIXED = nsIAccessibleStates.STATE_MIXED; +const STATE_MULTISELECTABLE = nsIAccessibleStates.STATE_MULTISELECTABLE; +const STATE_OFFSCREEN = nsIAccessibleStates.STATE_OFFSCREEN; +const STATE_PRESSED = nsIAccessibleStates.STATE_PRESSED; +const STATE_PROTECTED = nsIAccessibleStates.STATE_PROTECTED; +const STATE_READONLY = nsIAccessibleStates.STATE_READONLY; +const STATE_REQUIRED = nsIAccessibleStates.STATE_REQUIRED; +const STATE_SELECTABLE = nsIAccessibleStates.STATE_SELECTABLE; +const STATE_SELECTED = nsIAccessibleStates.STATE_SELECTED; +const STATE_TRAVERSED = nsIAccessibleStates.STATE_TRAVERSED; +const STATE_UNAVAILABLE = nsIAccessibleStates.STATE_UNAVAILABLE; + +const EXT_STATE_ACTIVE = nsIAccessibleStates.EXT_STATE_ACTIVE; +const EXT_STATE_CURRENT = nsIAccessibleStates.EXT_STATE_CURRENT; +const EXT_STATE_DEFUNCT = nsIAccessibleStates.EXT_STATE_DEFUNCT; +const EXT_STATE_EDITABLE = nsIAccessibleStates.EXT_STATE_EDITABLE; +const EXT_STATE_ENABLED = nsIAccessibleStates.EXT_STATE_ENABLED; +const EXT_STATE_EXPANDABLE = nsIAccessibleStates.EXT_STATE_EXPANDABLE; +const EXT_STATE_HORIZONTAL = nsIAccessibleStates.EXT_STATE_HORIZONTAL; +const EXT_STATE_MODAL = nsIAccessibleStates.EXT_STATE_MODAL; +const EXT_STATE_MULTI_LINE = nsIAccessibleStates.EXT_STATE_MULTI_LINE; +const EXT_STATE_PINNED = nsIAccessibleStates.EXT_STATE_PINNED; +const EXT_STATE_SENSITIVE = nsIAccessibleStates.EXT_STATE_SENSITIVE; +const EXT_STATE_SINGLE_LINE = nsIAccessibleStates.EXT_STATE_SINGLE_LINE; +const EXT_STATE_STALE = nsIAccessibleStates.EXT_STATE_STALE; +const EXT_STATE_SUPPORTS_AUTOCOMPLETION = + nsIAccessibleStates.EXT_STATE_SUPPORTS_AUTOCOMPLETION; +const EXT_STATE_VERTICAL = nsIAccessibleStates.EXT_STATE_VERTICAL; +const EXT_STATE_SELECTABLE_TEXT = nsIAccessibleStates.EXT_STATE_SELECTABLE_TEXT; + +const kOrdinalState = false; +const kExtraState = 1; + +// ////////////////////////////////////////////////////////////////////////////// +// Test functions + +/** + * Tests the states and extra states of the given accessible. + * Also tests for unwanted states and extra states. + * In addition, the function performs a few plausibility checks derived from the + * sstates and extra states passed in. + * + * @param aAccOrElmOrID The accessible, DOM element or ID to be tested. + * @param aState The state bits that are wanted. + * @param aExtraState The extra state bits that are wanted. + * @param aAbsentState State bits that are not wanted. + * @param aAbsentExtraState Extra state bits that are not wanted. + * @param aTestName The test name. + */ +function testStates( + aAccOrElmOrID, + aState, + aExtraState, + aAbsentState, + aAbsentExtraState, + aTestName +) { + var [state, extraState] = getStates(aAccOrElmOrID); + var role = getRole(aAccOrElmOrID); + var id = + prettyName(aAccOrElmOrID) + (aTestName ? " [" + aTestName + "]" : ""); + + // Primary test. + if (aState) { + isState(state & aState, aState, false, "wrong state bits for " + id + "!"); + } + + if (aExtraState) { + isState( + extraState & aExtraState, + aExtraState, + true, + "wrong extra state bits for " + id + "!" + ); + } + + if (aAbsentState) { + isState( + state & aAbsentState, + 0, + false, + "state bits should not be present in ID " + id + "!" + ); + } + + if (aAbsentExtraState) { + isState( + extraState & aAbsentExtraState, + 0, + true, + "extraState bits should not be present in ID " + id + "!" + ); + } + + // Additional test. + + // focused/focusable + if (state & STATE_FOCUSED) { + isState( + state & STATE_FOCUSABLE, + STATE_FOCUSABLE, + false, + "Focussed " + id + " must be focusable!" + ); + } + + if (aAbsentState && aAbsentState & STATE_FOCUSABLE) { + isState( + state & STATE_FOCUSED, + 0, + false, + "Not focusable " + id + " must be not focused!" + ); + } + + // multiline/singleline + if (extraState & EXT_STATE_MULTI_LINE) { + isState( + extraState & EXT_STATE_SINGLE_LINE, + 0, + true, + "Multiline " + id + " cannot be singleline!" + ); + } + + if (extraState & EXT_STATE_SINGLE_LINE) { + isState( + extraState & EXT_STATE_MULTI_LINE, + 0, + true, + "Singleline " + id + " cannot be multiline!" + ); + } + + // expanded/collapsed/expandable + if (state & STATE_COLLAPSED || state & STATE_EXPANDED) { + isState( + extraState & EXT_STATE_EXPANDABLE, + EXT_STATE_EXPANDABLE, + true, + "Collapsed or expanded " + id + " must be expandable!" + ); + } + + if (state & STATE_COLLAPSED) { + isState( + state & STATE_EXPANDED, + 0, + false, + "Collapsed " + id + " cannot be expanded!" + ); + } + + if (state & STATE_EXPANDED) { + isState( + state & STATE_COLLAPSED, + 0, + false, + "Expanded " + id + " cannot be collapsed!" + ); + } + + if (aAbsentState && extraState & EXT_STATE_EXPANDABLE) { + if (aAbsentState & STATE_EXPANDED) { + isState( + state & STATE_COLLAPSED, + STATE_COLLAPSED, + false, + "Not expanded " + id + " must be collapsed!" + ); + } else if (aAbsentState & STATE_COLLAPSED) { + isState( + state & STATE_EXPANDED, + STATE_EXPANDED, + false, + "Not collapsed " + id + " must be expanded!" + ); + } + } + + // checked/mixed/checkable + if ( + state & STATE_CHECKED || + (state & STATE_MIXED && + role != ROLE_TOGGLE_BUTTON && + role != ROLE_PROGRESSBAR) + ) { + isState( + state & STATE_CHECKABLE, + STATE_CHECKABLE, + false, + "Checked or mixed element must be checkable!" + ); + } + + if (state & STATE_CHECKED) { + isState( + state & STATE_MIXED, + 0, + false, + "Checked element cannot be state mixed!" + ); + } + + if (state & STATE_MIXED) { + isState( + state & STATE_CHECKED, + 0, + false, + "Mixed element cannot be state checked!" + ); + } + + // selected/selectable + if (state & STATE_SELECTED && !(aAbsentState & STATE_SELECTABLE)) { + isState( + state & STATE_SELECTABLE, + STATE_SELECTABLE, + false, + "Selected element must be selectable!" + ); + } +} + +/** + * Tests an accessible and its sub tree for the passed in state bits. + * Used to make sure that states are propagated to descendants, for example the + * STATE_UNAVAILABLE from a container to its children. + * + * @param aAccOrElmOrID The accessible, DOM element or ID to be tested. + * @param aState The state bits that are wanted. + * @param aExtraState The extra state bits that are wanted. + * @param aAbsentState State bits that are not wanted. + */ +function testStatesInSubtree(aAccOrElmOrID, aState, aExtraState, aAbsentState) { + // test accessible and its subtree for propagated states. + var acc = getAccessible(aAccOrElmOrID); + if (!acc) { + return; + } + + if (getRole(acc) != ROLE_TEXT_LEAF) { + // Right now, text leafs don't get tested because the states are not being + // propagated. + testStates(acc, aState, aExtraState, aAbsentState); + } + + // Iterate over its children to see if the state got propagated. + var children = null; + try { + children = acc.children; + } catch (e) {} + ok(children, "Could not get children for " + aAccOrElmOrID + "!"); + + if (children) { + for (var i = 0; i < children.length; i++) { + var childAcc = children.queryElementAt(i, nsIAccessible); + testStatesInSubtree(childAcc, aState, aExtraState, aAbsentState); + } + } +} + +/** + * Fails if no defunct state on the accessible. + */ +function testIsDefunct(aAccessible, aTestName) { + var id = prettyName(aAccessible) + (aTestName ? " [" + aTestName + "]" : ""); + var [, /* state*/ extraState] = getStates(aAccessible); + isState( + extraState & EXT_STATE_DEFUNCT, + EXT_STATE_DEFUNCT, + true, + "no defuct state for " + id + "!" + ); +} + +function getStringStates(aAccOrElmOrID) { + var [state, extraState] = getStates(aAccOrElmOrID); + return statesToString(state, extraState); +} + +function getStates(aAccOrElmOrID) { + var acc = getAccessible(aAccOrElmOrID); + if (!acc) { + return [0, 0]; + } + + var state = {}, + extraState = {}; + acc.getState(state, extraState); + + return [state.value, extraState.value]; +} + +/** + * Return true if the accessible has given states. + */ +function hasState(aAccOrElmOrID, aState, aExtraState) { + var [state, exstate] = getStates(aAccOrElmOrID); + return ( + (aState ? state & aState : true) && + (aExtraState ? exstate & aExtraState : true) + ); +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private implementation details + +/** + * Analogy of SimpleTest.is function used to compare states. + */ +function isState(aState1, aState2, aIsExtraStates, aMsg) { + if (aState1 == aState2) { + ok(true, aMsg); + return; + } + + var got = "0"; + if (aState1) { + got = statesToString( + aIsExtraStates ? 0 : aState1, + aIsExtraStates ? aState1 : 0 + ); + } + + var expected = "0"; + if (aState2) { + expected = statesToString( + aIsExtraStates ? 0 : aState2, + aIsExtraStates ? aState2 : 0 + ); + } + + ok(false, aMsg + "got '" + got + "', expected '" + expected + "'"); +} diff --git a/accessible/tests/mochitest/states/a11y.ini b/accessible/tests/mochitest/states/a11y.ini new file mode 100644 index 0000000000..06e7a5a061 --- /dev/null +++ b/accessible/tests/mochitest/states/a11y.ini @@ -0,0 +1,36 @@ +[DEFAULT] +support-files = + z_frames.html + z_frames_article.html + z_frames_checkbox.html + z_frames_textbox.html + z_frames_update.html + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/dumbfile.zip + !/accessible/tests/mochitest/formimage.png + !/accessible/tests/mochitest/treeview.css + +[test_aria.html] +[test_aria.xhtml] +[test_aria_imgmap.html] +[test_aria_widgetitems.html] +[test_buttons.html] +[test_controls.html] +[test_controls.xhtml] +[test_doc.html] +[test_doc_busy.html] +[test_docarticle.html] +[test_editablebody.html] +[test_expandable.xhtml] +[test_frames.html] +[test_inputs.html] +[test_link.html] +[test_popup.xhtml] +[test_selects.html] +[test_stale.html] +[test_tabs.xhtml] +[test_textbox.xhtml] +[test_tree.xhtml] +[test_visibility.html] +[test_visibility.xhtml] +skip-if = asan # Bug 1199631 diff --git a/accessible/tests/mochitest/states/test_aria.html b/accessible/tests/mochitest/states/test_aria.html new file mode 100644 index 0000000000..da5df6fa09 --- /dev/null +++ b/accessible/tests/mochitest/states/test_aria.html @@ -0,0 +1,655 @@ +<html> + +<head> + <title>ARIA based nsIAccessible states testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style type="text/css"> + .offscreen { + position: absolute; + left: -5000px; + top: -5000px; + height: 100px; + width: 100px; + } + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function testAriaDisabledTree(aAccOrElmOrID) { + // test accessible and its subtree for propagated state. + var acc = getAccessible(aAccOrElmOrID); + if (!acc) + return; + + var [state /* extraState */] = getStates(aAccOrElmOrID); + if (state & STATE_UNAVAILABLE) { + var role = getRole(acc); + if (role != ROLE_GROUPING) { + testStates(acc, STATE_FOCUSABLE); + } + } + + // Iterate over its children to see if the state got propagated. + var children = null; + try { + children = acc.children; + } catch (e) {} + ok(children, "Could not get children for " + aAccOrElmOrID + "!"); + + if (children) { + for (var i = 0; i < children.length; i++) { + var childAcc = children.queryElementAt(i, nsIAccessible); + testAriaDisabledTree(childAcc); + } + } + } + + function doTest() { + // aria_autocomplete + testStates("textbox_autocomplete_inline", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("textbox_autocomplete_list", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("textbox_autocomplete_both", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("combobox_autocomplete_inline", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("combobox_autocomplete_list", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("combobox_autocomplete_both", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + + testStates("htmltext_autocomplete_list", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("htmltextarea_autocomplete_list", STATE_HASPOPUP, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + + // aria-busy + testStates("textbox_busy_false", 0, 0, STATE_BUSY); + testStates("textbox_busy_true", STATE_BUSY); + testStates("textbox_busy_error", STATE_INVALID); + + // aria-expanded + testStates("combobox", STATE_COLLAPSED); + testStates("combobox_expanded", STATE_EXPANDED); + + // tri-state checkbox + var checkboxElem = getNode("check1"); + if (checkboxElem) { + testStates(checkboxElem, STATE_CHECKED); + checkboxElem.checked = false; + testStates(checkboxElem, 0, 0, STATE_CHECKED); + checkboxElem.indeterminate = true; + testStates(checkboxElem, STATE_MIXED, 0); + } + + // aria-checked + testStates("aria_checked_checkbox", STATE_CHECKED); + testStates("aria_mixed_checkbox", STATE_MIXED); + testStates("aria_checked_switch", STATE_CHECKED); + testStates("aria_mixed_switch", 0, 0, STATE_MIXED); // unsupported + testStates("aria_mixed_switch", 0, 0, STATE_CHECKED); // not checked due to being unsupported + + // test disabled group and all its descendants to see if they are + // disabled, too. See bug 429285. + testAriaDisabledTree("group"); + + // aria-modal + testStates("aria_modal", 0, EXT_STATE_MODAL); + testStates("aria_modal_false", 0, 0, 0, EXT_STATE_MODAL); + + // aria-multiline + testStates("aria_multiline_textbox", 0, EXT_STATE_MULTI_LINE); + + // aria-multiselectable + testStates("aria_multiselectable_listbox", + STATE_MULTISELECTABLE | STATE_EXTSELECTABLE); + testStates("aria_multiselectable_tablist", + STATE_MULTISELECTABLE | STATE_EXTSELECTABLE); + + // aria-pressed + testStates("aria_pressed_button", STATE_PRESSED, 0, STATE_CHECKABLE); + testStates("aria_pressed_native_button", STATE_PRESSED, 0, STATE_CHECKABLE); + + // aria-readonly + testStates("aria_readonly_textbox", STATE_READONLY); + + // readonly on grid and gridcell + testStates("aria_grid_default", 0, 0, + STATE_READONLY, 0); + testStates("aria_grid_default_colheader_readonly", STATE_READONLY, 0, + 0, 0); + testStates("aria_grid_default_colheader_inherited", 0, 0, + STATE_READONLY, 0); + testStates("aria_grid_default_rowheader_readonly", STATE_READONLY, 0, + 0, 0); + testStates("aria_grid_default_rowheader_inherited", 0, 0, + STATE_READONLY, 0); + testStates("aria_grid_default_cell_readonly", STATE_READONLY, 0, + 0, 0); + testStates("aria_grid_default_cell_inherited", 0, 0, + STATE_READONLY, 0); + + testStates("aria_grid_readonly", STATE_READONLY, 0, + 0, 0); + testStates("aria_grid_readonly_colheader_editable", 0, 0, + STATE_READONLY, 0); + testStates("aria_grid_readonly_colheader_inherited", STATE_READONLY, 0, + 0, 0); + testStates("aria_grid_readonly_rowheader_editable", 0, 0, + STATE_READONLY, 0); + testStates("aria_grid_readonly_rowheader_inherited", STATE_READONLY, 0, + 0, 0); + testStates("aria_grid_readonly_cell_editable", 0, 0, + STATE_READONLY, 0); + testStates("aria_grid_readonly_cell_inherited", STATE_READONLY, 0, + 0, 0); + + // readonly on treegrid and gridcell + testStates("aria_treegrid_default", 0, 0, + STATE_READONLY, 0); + testStates("aria_treegrid_default_colheader_readonly", STATE_READONLY, 0, + 0, 0); + testStates("aria_treegrid_default_colheader_inherited", 0, 0, + STATE_READONLY, 0); + testStates("aria_treegrid_default_rowheader_readonly", STATE_READONLY, 0, + 0, 0); + testStates("aria_treegrid_default_rowheader_inherited", 0, 0, + STATE_READONLY, 0); + testStates("aria_treegrid_default_cell_readonly", STATE_READONLY, 0, + 0, 0); + testStates("aria_treegrid_default_cell_inherited", 0, 0, + STATE_READONLY, 0); + + testStates("aria_treegrid_readonly", STATE_READONLY, 0, + 0, 0); + testStates("aria_treegrid_readonly_colheader_editable", 0, 0, + STATE_READONLY, 0); + testStates("aria_treegrid_readonly_colheader_inherited", STATE_READONLY, 0, + 0, 0); + testStates("aria_treegrid_readonly_rowheader_editable", 0, 0, + STATE_READONLY, 0); + testStates("aria_treegrid_readonly_rowheader_inherited", STATE_READONLY, 0, + 0, 0); + testStates("aria_treegrid_readonly_cell_editable", 0, 0, + STATE_READONLY, 0); + testStates("aria_treegrid_readonly_cell_inherited", STATE_READONLY, 0, + 0, 0); + + // aria-readonly on directory + testStates("aria_directory", STATE_READONLY); + + // aria-selectable + testStates("aria_selectable_listitem", STATE_SELECTABLE | STATE_SELECTED); + + // active state caused by aria-activedescendant + testStates("as_item1", 0, EXT_STATE_ACTIVE); + testStates("as_item2", 0, 0, 0, EXT_STATE_ACTIVE); + + // universal ARIA properties inherited from file input control + var fileBrowseButton = getAccessible("fileinput").firstChild; + testStates(fileBrowseButton, + STATE_BUSY | STATE_UNAVAILABLE | STATE_REQUIRED | STATE_HASPOPUP | STATE_INVALID); + // No states on the label. + + // offscreen test + testStates("aria_offscreen_textbox", STATE_OFFSCREEN); + + // + // This section tests aria roles on links/anchors for underlying + // HTMLLinkAccessible creation. (see closed bug 494807) + // + + // strong roles + testStates("aria_menuitem_link", 0, 0, STATE_LINKED); + testStates("aria_button_link", 0, 0, STATE_LINKED); + testStates("aria_checkbox_link", 0, 0, STATE_LINKED); + + // strong landmark + testStates("aria_application_link", 0, 0, STATE_LINKED); + testStates("aria_application_anchor", 0, 0, STATE_SELECTABLE); + + // strange cases + testStates("aria_link_link", STATE_LINKED); + testStates("aria_link_anchor", STATE_SELECTABLE); + + // some landmarks that break accessibility for these native elements + // Note that these are illegal uses by web authors as per WAI-ARIA in HTML + testStates("aria_main_link", 0, 0, STATE_LINKED); + testStates("aria_navigation_link", 0, 0, STATE_LINKED); + testStates("aria_main_anchor", 0, 0, STATE_SELECTABLE); + testStates("aria_navigation_anchor", 0, 0, STATE_SELECTABLE); + + // aria-orientation + testStates("aria_combobox", 0, 0, 0, EXT_STATE_HORIZONTAL | EXT_STATE_VERTICAL); + testStates("aria_hcombobox", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vcombobox", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_listbox", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_hlistbox", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vlistbox", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_menu", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_hmenu", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vmenu", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_menubar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_hmenubar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vmenubar", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_radiogroup", 0, 0, 0, EXT_STATE_HORIZONTAL | EXT_STATE_VERTICAL); + testStates("aria_hradiogroup", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vradiogroup", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_scrollbar", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_hscrollbar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vscrollbar", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_separator", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_hseparator", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vseparator", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_slider", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_hslider", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vslider", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_tablist", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_htablist", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vtablist", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_toolbar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_htoolbar", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vtoolbar", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_tree", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_htree", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vtree", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + testStates("aria_treegrid", 0, 0, 0, EXT_STATE_HORIZONTAL | EXT_STATE_VERTICAL); + testStates("aria_htreegrid", 0, EXT_STATE_HORIZONTAL, 0, EXT_STATE_VERTICAL); + testStates("aria_vtreegrid", 0, EXT_STATE_VERTICAL, 0, EXT_STATE_HORIZONTAL); + + // indeterminate ARIA progressbars (no aria-valuenow or aria-valuetext attribute) + // should expose mixed state + testStates("aria_progressbar", STATE_MIXED); + testStates("aria_progressbar_valuenow", 0, 0, STATE_MIXED); + testStates("aria_progressbar_valuetext", 0, 0, STATE_MIXED); + + // aria-current + testStates("current_page_1", 0, EXT_STATE_CURRENT); + testStates("page_2", 0, 0, EXT_STATE_CURRENT); + testStates("page_3", 0, 0, EXT_STATE_CURRENT); + testStates("page_4", 0, 0, EXT_STATE_CURRENT); + testStates("current_foo", 0, EXT_STATE_CURRENT); + + testStates("aria_listbox", STATE_FOCUSABLE); + testStates("aria_grid", STATE_FOCUSABLE); + testStates("aria_tree", STATE_FOCUSABLE); + testStates("aria_treegrid", STATE_FOCUSABLE); + testStates("aria_listbox_disabled", 0, 0, STATE_FOCUSABLE); + testStates("aria_grid_disabled", 0, 0, STATE_FOCUSABLE); + testStates("aria_tree_disabled", 0, 0, STATE_FOCUSABLE); + testStates("aria_treegrid_disabled", 0, 0, STATE_FOCUSABLE); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=457219" + title="nsIAccessible states testing"> + Mozilla Bug 457219 + </a><br /> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=429285" + title="Propagate aria-disabled to descendants"> + Mozilla Bug 429285 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=457226" + title="Mochitests for ARIA states"> + Mozilla Bug 457226 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=499653" + title="Unify ARIA state attributes mapping rules"> + Mozilla Bug 499653 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=681674" + title="aria-autocomplete not supported on standard form text input controls"> + Mozilla Bug 681674 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=681674" + title="aria-orientation should be applied to separator and slider roles"> + Mozilla Bug 681674 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=689847" + title="Expose active state on current item of selectable widgets"> + Mozilla Bug 689847 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017" + title="File input control should be propogate states to descendants"> + Mozilla Bug 699017 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=690199" + title="ARIA select widget should expose focusable state regardless the way they manage its children"> + Mozilla Bug 690199 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=740851" + title="ARIA undetermined progressmeters should expose mixed state"> + Mozilla Bug 740851 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=762876" + title="fix default horizontal / vertical state of role=scrollbar and ensure only one of horizontal / vertical states is exposed"> + Mozilla Bug 762876 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=892091" + title="ARIA treegrid should be editable by default"> + Bug 892091 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=835121" + title="ARIA grid should be editable by default"> + Mozilla Bug 835121 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=989958" + title="Pressed state is not exposed on a button element with aria-pressed attribute"> + Mozilla Bug 989958 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1136563" + title="Support ARIA 1.1 switch role"> + Mozilla Bug 1136563 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1355921" + title="Elements with a defined, non-false value for aria-current should expose ATK_STATE_ACTIVE"> + Mozilla Bug 1355921 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="textbox_autocomplete_inline" role="textbox" aria-autocomplete="inline"></div> + <div id="textbox_autocomplete_list" role="textbox" aria-autocomplete="list"></div> + <div id="textbox_autocomplete_both" role="textbox" aria-autocomplete="both"></div> + <div id="combobox_autocomplete_inline" role="combobox" aria-autocomplete="inline"></div> + <div id="combobox_autocomplete_list" role="combobox" aria-autocomplete="list"></div> + <div id="combobox_autocomplete_both" role="combobox" aria-autocomplete="both"></div> + + <input id="htmltext_autocomplete_list" type="text" aria-autocomplete="list" /> + <textarea id="htmltextarea_autocomplete_list" aria-autocomplete="list"></textarea> + + <div id="textbox_busy_false" role="textbox" aria-busy="false"></div> + <div id="textbox_busy_true" role="textbox" aria-busy="true"></div> + <div id="textbox_busy_error" role="textbox" aria-busy="error"></div> + + <div id="combobox" role="combobox">combobox</div> + <div id="combobox_expanded" role="combobox" + aria-expanded="true">combobox</div> + + <input type="checkbox" id="check1" value="I agree" checked="true"/> + + <div id="aria_checked_checkbox" role="checkbox" aria-checked="true"> + I agree + </div> + + <div id="aria_mixed_checkbox" role="checkbox" aria-checked="mixed"> + I might agree + </div> + + <div id="aria_checked_switch" role="switch" aria-checked="true"> + I am switched on + </div> + + <div id="aria_mixed_switch" role="switch" aria-checked="mixed"> + I am unsupported + </div> + + <div id="aria_modal" aria-modal="true">modal stuff</div> + <div id="aria_modal_false" aria-modal="false">non modal stuff</div>div> + <div id="aria_multiline_textbox" role="textbox" aria-multiline="true"></div> + <div id="aria_multiselectable_listbox" role="listbox" aria-multiselectable="true"></div> + <div id="aria_multiselectable_tablist" role="tablist" aria-multiselectable="true"></div> + <div id="aria_pressed_button" role="button" aria-pressed="true">Button</div> + <button id="aria_pressed_native_button" aria-pressed="true">Button</button> + + <div id="aria_readonly_textbox" + role="textbox" aria-readonly="true">This text should be readonly</div> + + <div id="aria_grid_default" role="grid"> + <div role="row"> + <div id="aria_grid_default_colheader_readonly" + role="columnheader" aria-readonly="true">colheader1</div> + <div id="aria_grid_default_colheader_inherited" + role="columnheader">colheader2</div> + </div> + <div role="row"> + <div id="aria_grid_default_rowheader_readonly" + role="rowheader" aria-readonly="true">rowheader1</div> + <div id="aria_grid_default_rowheader_inherited" + role="rowheader">rowheader2</div> + </div> + <div role="row"> + <div id="aria_grid_default_cell_readonly" + role="gridcell" aria-readonly="true">gridcell1</div> + <div id="aria_grid_default_cell_inherited" + role="gridcell">gridcell2</div> + </div> + </div> + + <div id="aria_grid_readonly" role="grid" aria-readonly="true"> + <div role="row"> + <div id="aria_grid_readonly_colheader_editable" + role="columnheader" aria-readonly="false">colheader1</div> + <div id="aria_grid_readonly_colheader_inherited" + role="columnheader">colheader2</div> + </div> + <div role="row"> + <div id="aria_grid_readonly_rowheader_editable" + role="rowheader" aria-readonly="false">rowheader1</div> + <div id="aria_grid_readonly_rowheader_inherited" + role="rowheader">rowheader2</div> + </div> + <div role="row"> + <div id="aria_grid_readonly_cell_editable" + role="gridcell" aria-readonly="false">gridcell1</div> + <div id="aria_grid_readonly_cell_inherited" + role="gridcell">gridcell2</div> + </div> + </div> + + <div id="aria_treegrid_default" role="grid"> + <div role="row"> + <div id="aria_treegrid_default_colheader_readonly" + role="columnheader" aria-readonly="true">colheader1</div> + <div id="aria_treegrid_default_colheader_inherited" + role="columnheader">colheader2</div> + </div> + <div role="row"> + <div id="aria_treegrid_default_rowheader_readonly" + role="rowheader" aria-readonly="true">rowheader1</div> + <div id="aria_treegrid_default_rowheader_inherited" + role="rowheader">rowheader2</div> + </div> + <div role="row"> + <div id="aria_treegrid_default_cell_readonly" + role="gridcell" aria-readonly="true">gridcell1</div> + <div id="aria_treegrid_default_cell_inherited" + role="gridcell">gridcell2</div> + </div> + </div> + + <div id="aria_treegrid_readonly" role="grid" aria-readonly="true"> + <div role="row"> + <div id="aria_treegrid_readonly_colheader_editable" + role="columnheader" aria-readonly="false">colheader1</div> + <div id="aria_treegrid_readonly_colheader_inherited" + role="columnheader">colheader2</div> + </div> + <div role="row"> + <div id="aria_treegrid_readonly_rowheader_editable" + role="rowheader" aria-readonly="false">rowheader1</div> + <div id="aria_treegrid_readonly_rowheader_inherited" + role="rowheader">rowheader2</div> + </div> + <div role="row"> + <div id="aria_treegrid_readonly_cell_editable" + role="gridcell" aria-readonly="false">gridcell1</div> + <div id="aria_treegrid_readonly_cell_inherited" + role="gridcell">gridcell2</div> + </div> + </div> + + <div role="listbox"> + <div id="aria_selectable_listitem" role="option" aria-selected="true">Item1</div> + </div> + + <!-- Test that aria-disabled state gets propagated to all descendants --> + <div id="group" role="group" aria-disabled="true"> + <button>hi</button> + <div tabindex="0" role="listbox" aria-activedescendant="item1" + aria-owns="item5"> + <div role="option" id="item1">Item 1</div> + <div role="option" id="item2">Item 2</div> + <div role="option" id="item3">Item 3</div> + <div role="option" id="item4">Item 4</div> + </div> + <div role="slider" tabindex="0">A slider</div> + </div> + <div role="option" id="item5">Item 5</div> + + <!-- Test active state --> + <div id="as_listbox" tabindex="0" role="listbox" + aria-activedescendant="as_item1"> + <div role="option" id="as_item1">Item 1</div> + <div role="option" id="as_item2">Item 2</div> + </div> + + <!-- universal ARIA properties should be inherited by text field of file input --> + <input type="file" id="fileinput" + aria-busy="true" + aria-disabled="true" + aria-required="true" + aria-haspopup="true" + aria-invalid="true"> + + <div id="offscreen_log" role="log" class="offscreen"> + <div id="aria_offscreen_textbox" role="textbox" aria-readonly="true">This text should be offscreen</div> + </div> + + <a id="aria_menuitem_link" role="menuitem" href="foo">menuitem</a> + <a id="aria_button_link" role="button" href="foo">button</a> + <a id="aria_checkbox_link" role="checkbox" href="foo">checkbox</a> + + <!-- strange edge case: please don't do this in the wild --> + <a id="aria_link_link" role="link" href="foo">link</a> + <a id="aria_link_anchor" role="link" name="link_anchor">link</a> + + <!-- landmarks: links --> + <a id="aria_application_link" role="application" href="foo">app</a> + <a id="aria_main_link" role="main" href="foo">main</a> + <a id="aria_navigation_link" role="navigation" href="foo">nav</a> + + <!-- landmarks: anchors --> + <a id="aria_application_anchor" role="application" name="app_anchor">app</a> + <a id="aria_main_anchor" role="main" name="main_anchor">main</a> + <a id="aria_navigation_anchor" role="navigation" name="nav_anchor">nav</a> + + <!-- aria-orientation --> + <div id="aria_combobox" role="combobox">combobox</div> + <div id="aria_hcombobox" role="combobox" aria-orientation="horizontal">horizontal combobox</div> + <div id="aria_vcombobox" role="combobox" aria-orientation="vertical">vertical combobox</div> + <div id="aria_listbox" role="listbox">listbox</div> + <div id="aria_hlistbox" role="listbox" aria-orientation="horizontal">horizontal listbox</div> + <div id="aria_vlistbox" role="listbox" aria-orientation="vertical">vertical listbox</div> + <div id="aria_menu" role="menu">menu</div> + <div id="aria_hmenu" role="menu" aria-orientation="horizontal">horizontal menu</div> + <div id="aria_vmenu" role="menu" aria-orientation="vertical">vertical menu</div> + <div id="aria_menubar" role="menubar">menubar</div> + <div id="aria_hmenubar" role="menubar" aria-orientation="horizontal">horizontal menubar</div> + <div id="aria_vmenubar" role="menubar" aria-orientation="vertical">vertical menubar</div> + <div id="aria_radiogroup" role="radiogroup">radiogroup</div> + <div id="aria_hradiogroup" role="radiogroup" aria-orientation="horizontal">horizontal radiogroup</div> + <div id="aria_vradiogroup" role="radiogroup" aria-orientation="vertical">vertical radiogroup</div> + <div id="aria_scrollbar" role="scrollbar">scrollbar</div> + <div id="aria_hscrollbar" role="scrollbar" aria-orientation="horizontal">horizontal scrollbar</div> + <div id="aria_vscrollbar" role="scrollbar" aria-orientation="vertical">vertical scrollbar</div> + <div id="aria_separator" role="separator">separator</div> + <div id="aria_hseparator" role="separator" aria-orientation="horizontal">horizontal separator</div> + <div id="aria_vseparator" role="separator" aria-orientation="vertical">vertical separator</div> + <div id="aria_slider" role="slider">slider</div> + <div id="aria_hslider" role="slider" aria-orientation="horizontal">horizontal slider</div> + <div id="aria_vslider" role="slider" aria-orientation="vertical">vertical slider</div> + + <div id="aria_tablist" role="tablist">tablist</div> + <div id="aria_htablist" role="tablist" aria-orientation="horizontal">horizontal tablist</div> + <div id="aria_vtablist" role="tablist" aria-orientation="vertical">vertical tablist</div> + <div id="aria_toolbar" role="toolbar">toolbar</div> + <div id="aria_htoolbar" role="toolbar" aria-orientation="horizontal">horizontal toolbar</div> + <div id="aria_vtoolbar" role="toolbar" aria-orientation="vertical">vertical toolbar</div> + <div id="aria_tree" role="tree">tree</div> + <div id="aria_htree" role="tree" aria-orientation="horizontal">horizontal tree</div> + <div id="aria_vtree" role="tree" aria-orientation="vertical">vertical tree</div> + <div id="aria_treegrid" role="treegrid">treegrid</div> + <div id="aria_htreegrid" role="treegrid" aria-orientation="horizontal">horizontal treegrid</div> + <div id="aria_vtreegrid" role="treegrid" aria-orientation="vertical">vertical treegrid</div> + + <!-- indeterminate ARIA progressbars should expose mixed state --> + <div id="aria_progressbar" role="progressbar"></div> + <div id="aria_progressbar_valuenow" role="progressbar" aria-valuenow="1"></div> + <div id="aria_progressbar_valuetext" role="progressbar" aria-valuetext="value"></div> + + <!-- ARIA select widget should expose focusable state regardless the way they manage its children --> + <div id="aria_listbox" role="listbox"> + <div role="option" tabindex="0">A</div> + <div role="option" tabindex="0">a</div> + </div> + <div id="aria_grid" role="grid"> + <div role="row"><div role="gridcell" tabindex="0">B</div></div></div> + <div role="row"><div role="gridcell" tabindex="0">b</div></div></div> + <div id="aria_tree" role="tree"> + <div role="treeitem" tabindex="0">C</div> + <div role="treeitem" tabindex="0">c</div> + </div> + <div id="aria_treegrid" role="treegrid"> + <div role="row"><div role="gridcell" tabindex="0">D</div></div> + <div role="row"><div role="gridcell" tabindex="0">d</div></div> + </div> + <div id="aria_listbox_disabled" role="listbox" aria-disabled="true"> + <div role="option">E</div> + <div role="option">e</div> + </div> + <div id="aria_grid_disabled" role="grid" aria-disabled="true"> + <div role="row"><div role="gridcell">F</div></div> + <div role="row"><div role="gridcell">f</div></div> + </div> + <div id="aria_tree_disabled" role="tree" aria-disabled="true"> + <div role="treeitem">G</div> + <div role="treeitem">g</div> + </div> + <div id="aria_treegrid_disabled" role="treegrid" aria-disabled="true"> + <div role="row"><div role="gridcell">H</div></div> + <div role="row"><div role="gridcell">h</div></div> + </div> + + <!-- Test that directory is readonly --> + <div id="aria_directory" role="directory"></div> + + <!-- aria-current --> + <div id="current_page_1" role="link" aria-current="page">1</div> + <div id="page_2" role="link" aria-current="false">2</div> + <div id="page_3" role="link">3</div> + <div id="page_4" role="link" aria-current="">4</div> + <div id="current_foo" aria-current="foo">foo</div> +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_aria.xhtml b/accessible/tests/mochitest/states/test_aria.xhtml new file mode 100644 index 0000000000..e42a0ea96d --- /dev/null +++ b/accessible/tests/mochitest/states/test_aria.xhtml @@ -0,0 +1,70 @@ +<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css"
+ type="text/css"?>
+
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="Accessible XUL ARIA state tests">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" />
+
+ <script type="application/javascript"
+ src="../common.js" />
+ <script type="application/javascript"
+ src="../role.js" />
+ <script type="application/javascript"
+ src="../states.js" />
+
+ <script type="application/javascript">
+ <![CDATA[
+ function doTest()
+ {
+ // aria-pressed
+ testStates("pressed_button", STATE_PRESSED, 0, STATE_CHECKABLE);
+
+ testStates("tabs", STATE_MULTISELECTABLE);
+ // Make sure XUL selection works, since aria-selected defaults to false.
+ testStates("tab1", STATE_SELECTED);
+ // aria-selected="true".
+ testStates("tab2", STATE_SELECTED);
+ // Neither.
+ testStates("tab3", 0, 0, STATE_SELECTED);
+
+ SimpleTest.finish()
+ }
+
+ SimpleTest.waitForExplicitFinish();
+ addA11yLoadEvent(doTest);
+ ]]>
+ </script>
+
+ <hbox flex="1" style="overflow: auto;">
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <a target="_blank"
+ href="https://bugzilla.mozilla.org/show_bug.cgi?id=1033283"
+ title="Expose pressed state on XUL menu toggle buttons">
+ Mozilla Bug 1033283
+ </a>
+ <p id="display"></p>
+ <div id="content" style="display: none">
+ </div>
+ <pre id="test">
+ </pre>
+ </body>
+
+ <vbox flex="1">
+ <button id="pressed_button" aria-pressed="true" label="I am pressed" />
+
+ <tabbox>
+ <tabs id="tabs" aria-multiselectable="true">
+ <tab id="tab1" label="tab1" selected="true"/>
+ <tab id="tab2" label="tab2" aria-selected="true"/>
+ <tab id="tab3" label="tab3"/>
+ </tabs>
+ </tabbox>
+ </vbox>
+ </hbox>
+
+</window>
+
diff --git a/accessible/tests/mochitest/states/test_aria_imgmap.html b/accessible/tests/mochitest/states/test_aria_imgmap.html new file mode 100644 index 0000000000..387e33259e --- /dev/null +++ b/accessible/tests/mochitest/states/test_aria_imgmap.html @@ -0,0 +1,75 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test usemap elements and ARIA</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + function doPreTest() { + waitForImageMap("imagemap", doTest); + } + + function doTest() { + var imageMap = getAccessible("imagemap"); + + var t1 = imageMap.getChildAt(0); + testStates(t1, 0, EXT_STATE_EDITABLE, STATE_LINKED); + var t2 = imageMap.getChildAt(1); + testStates(t2, 0, EXT_STATE_EDITABLE, STATE_LINKED); + var rb1 = imageMap.getChildAt(2); + testStates(rb1, (STATE_CHECKABLE | STATE_CHECKED), 0, STATE_LINKED); + var rb2 = imageMap.getChildAt(3); + testStates(rb2, STATE_CHECKABLE, 0, STATE_CHECKED, STATE_LINKED); + var cb1 = imageMap.getChildAt(4); + testStates(cb1, (STATE_CHECKABLE | STATE_CHECKED), 0, STATE_LINKED); + var cbox = imageMap.getChildAt(5); + testStates(cbox, (STATE_HASPOPUP | STATE_COLLAPSED), + EXT_STATE_EXPANDABLE, STATE_LINKED); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doPreTest); + </script> + +</head> +<body> + +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=548291" + title="ARIA states on image maps"> +Mozilla Bug 548291 +</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> + +<img id="imagemap" src="../formimage.png" width="219" height="229" border="0" usemap="#ariaMap"> +<map id="ariaMap" name="ariaMap"> + <area id="t1" role="textbox" shape="rect" tabindex="0" alt="" title="first name" coords="4,20,108,48" href="#" /> + <area id="t2" role="textbox" shape="rect" alt="" title="last name" coords="111,21,215,50" href="#" /> + <area id="rb1" role="radio" aria-checked="true" shape="circle" alt="" title="male" coords="60,75,11" href="#" /> + <area id="rb2" role="radio" shape="circle" alt="" title="female" coords="73,94,11" href="#" /> + <area id="cb1" role="checkbox" aria-checked="true" shape="rect" alt="" title="have bike" coords="95,123,118,145" href="#" /> + <area id="cbox" role="combobox" shape="rect" alt="" title="bike model" coords="120,124,184,146" href="#" /> + <area id="cb2" role="checkbox" shape="rect" alt="" title="have car" coords="90,145,114,164" href="#" /> + <area id="cb3" role="checkbox" shape="rect" alt="" title="have airplane" coords="130,163,152,184" href="#" /> + <area id="b1" role="button" shape="rect" alt="" title="submit" coords="4,198,67,224" href="#" /> +</map> +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_aria_widgetitems.html b/accessible/tests/mochitest/states/test_aria_widgetitems.html new file mode 100644 index 0000000000..c2d546ba01 --- /dev/null +++ b/accessible/tests/mochitest/states/test_aria_widgetitems.html @@ -0,0 +1,152 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Test ARIA tab accessible selected state</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function focusARIAItem(aID, aIsSelected) { + this.DOMNode = getNode(aID); + + this.invoke = function focusARIAItem_invoke() { + this.DOMNode.focus(); + }; + + this.check = function focusARIAItem_check(aEvent) { + testStates(this.DOMNode, aIsSelected ? STATE_SELECTED : 0, 0, + aIsSelected ? 0 : STATE_SELECTED); + }; + + this.getID = function focusARIAItem_getID() { + return "Focused ARIA widget item with aria-selected='" + + (aIsSelected ? "true', should" : "false', shouldn't") + + " have selected state on " + prettyName(aID); + }; + } + + function focusActiveDescendantItem(aItemID, aWidgetID, aIsSelected) { + this.DOMNode = getNode(aItemID); + this.widgetDOMNode = getNode(aWidgetID); + + this.invoke = function focusActiveDescendantItem_invoke() { + this.widgetDOMNode.setAttribute("aria-activedescendant", aItemID); + this.widgetDOMNode.focus(); + }; + + this.check = function focusActiveDescendantItem_check(aEvent) { + testStates(this.DOMNode, aIsSelected ? STATE_SELECTED : 0, 0, + aIsSelected ? 0 : STATE_SELECTED); + }; + + this.getID = function tabActiveDescendant_getID() { + return "ARIA widget item managed by activedescendant " + + (aIsSelected ? "should" : "shouldn't") + + " have the selected state on " + prettyName(aItemID); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTest() { + // aria-selected + testStates("aria_tab1", 0, 0, STATE_SELECTED); + testStates("aria_tab2", STATE_SELECTED); + testStates("aria_tab3", 0, 0, STATE_SELECTED); + testStates("aria_option1", 0, 0, STATE_SELECTED); + testStates("aria_option2", STATE_SELECTED); + testStates("aria_option3", 0, 0, STATE_SELECTED); + testStates("aria_treeitem1", 0, 0, STATE_SELECTED); + testStates("aria_treeitem2", STATE_SELECTED); + testStates("aria_treeitem3", 0, 0, STATE_SELECTED); + + // selected state when widget item is focused + gQueue = new eventQueue(EVENT_FOCUS); + + gQueue.push(new focusARIAItem("aria_tab1", true)); + gQueue.push(new focusARIAItem("aria_tab2", true)); + gQueue.push(new focusARIAItem("aria_tab3", false)); + gQueue.push(new focusARIAItem("aria_option1", true)); + gQueue.push(new focusARIAItem("aria_option2", true)); + gQueue.push(new focusARIAItem("aria_option3", false)); + gQueue.push(new focusARIAItem("aria_treeitem1", true)); + gQueue.push(new focusARIAItem("aria_treeitem2", true)); + gQueue.push(new focusARIAItem("aria_treeitem3", false)); + + // selected state when widget item is focused (by aria-activedescendant) + gQueue.push(new focusActiveDescendantItem("aria_tab5", "aria_tablist2", true)); + gQueue.push(new focusActiveDescendantItem("aria_tab6", "aria_tablist2", true)); + gQueue.push(new focusActiveDescendantItem("aria_tab4", "aria_tablist2", false)); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=653601" + title="aria-selected ignored for ARIA tabs"> + Mozilla Bug 653601 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=526703" + title="Focused widget item should expose selected state by default"> + Mozilla Bug 526703 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- tab --> + <div id="aria_tablist" role="tablist"> + <div id="aria_tab1" role="tab" tabindex="0">unselected tab</div> + <div id="aria_tab2" role="tab" tabindex="0" aria-selected="true">selected tab</div> + <div id="aria_tab3" role="tab" tabindex="0" aria-selected="false">focused explicitly unselected tab</div> + </div> + + <!-- listbox --> + <div id="aria_listbox" role="listbox"> + <div id="aria_option1" role="option" tabindex="0">unselected option</div> + <div id="aria_option2" role="option" tabindex="0" aria-selected="true">selected option</div> + <div id="aria_option3" role="option" tabindex="0" aria-selected="false">focused explicitly unselected option</div> + </div> + + <!-- tree --> + <div id="aria_tree" role="tree"> + <div id="aria_treeitem1" role="treeitem" tabindex="0">unselected treeitem</div> + <div id="aria_treeitem2" role="treeitem" tabindex="0" aria-selected="true">selected treeitem</div> + <div id="aria_treeitem3" role="treeitem" tabindex="0" aria-selected="false">focused explicitly unselected treeitem</div> + </div> + + <!-- tab managed by active-descendant --> + <div id="aria_tablist2" role="tablist" tabindex="0"> + <div id="aria_tab4" role="tab" aria-selected="false">focused explicitly unselected tab</div> + <div id="aria_tab5" role="tab">initially selected tab</div> + <div id="aria_tab6" role="tab">later selected tab</div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_buttons.html b/accessible/tests/mochitest/states/test_buttons.html new file mode 100644 index 0000000000..ec6e65cf32 --- /dev/null +++ b/accessible/tests/mochitest/states/test_buttons.html @@ -0,0 +1,83 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML button accessible states</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function doTest() { + // Default state. + testStates("f1_image", STATE_DEFAULT | STATE_FOCUSABLE); + testStates("f2_submit", STATE_DEFAULT | STATE_FOCUSABLE); + testStates("f3_submitbutton", STATE_DEFAULT | STATE_FOCUSABLE); + testStates("f3_disabled_reset", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE, 0); + testStates("f4_button", STATE_FOCUSABLE, 0, STATE_DEFAULT); + testStates("f4_disabled_button", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE, 0); + testStates("f4_image1", STATE_DEFAULT | STATE_FOCUSABLE); + testStates("f4_image2", STATE_FOCUSABLE, 0, STATE_DEFAULT); + testStates("f4_submit", STATE_FOCUSABLE, 0, STATE_DEFAULT); + testStates("f4_submitbutton", STATE_FOCUSABLE, 0, STATE_DEFAULT); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=664142" + title="DEFAULT state exposed incorrectly for HTML"> + Mozilla Bug 664142 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p>A form with an image button</p> + <form name="form1" method="get"> + <input type="text" name="hi"> + + <input id="f1_image" type="image" value="image-button"> + </form> + + <p>A form with a submit button:</p> + <form name="form2" method="get"> + <input type="text" name="hi"> + <input id="f2_submit" type="submit"> + </form> + + <p>A form with a HTML4 submit button:</p> + <form name="form3" method="get"> + <input type="text" name="hi"> + <button id="f3_submitbutton" type="submit">submit</button> + <button id="f3_disabled_reset" type="reset" disabled>reset</button> + </form> + + <p>A form with normal button, two image buttons, submit button, + HTML4 submit button:</p> + <form name="form4" method="get"> + <input type="text" name="hi"> + <input id="f4_button" type="button" value="normal" name="normal-button"> + <input id="f4_disabled_button" type="button" value="disabled" name="disabled-button" disabled> + <input id="f4_image1" type="image" value="image-button1" name="image-button1"> + <input id="f4_image2" type="image" value="image-button2" name="image-button2"> + <input id="f4_submit" type="submit" value="real-submit" name="real-submit"> + <button id="f4_submitbutton" type="submit">submit</button> + </form> + + </body> +</html> diff --git a/accessible/tests/mochitest/states/test_controls.html b/accessible/tests/mochitest/states/test_controls.html new file mode 100644 index 0000000000..27aecf4c52 --- /dev/null +++ b/accessible/tests/mochitest/states/test_controls.html @@ -0,0 +1,51 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML control states</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function doTest() { + // Undetermined progressbar (no value or aria-value attribute): mixed state + testStates("progress", STATE_MIXED); + // Determined progressbar (has value): shouldn't have mixed state + testStates("progress2", 0, 0, STATE_MIXED); + // Determined progressbar (has aria-value): shouldn't have mixed state + // testStates("progress3", 0, 0, STATE_MIXED); + todo(false, "we should respect ARIA"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=670853" + title="Bug 670853 - undetermined progressmeters should expose mixed state"> + Mozilla Bug 670853 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <progress id="progress"></progress> + <progress id="progress2" value="1"></progress> + <progress id="progress3" aria-valuenow="1"></progress> + +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_controls.xhtml b/accessible/tests/mochitest/states/test_controls.xhtml new file mode 100644 index 0000000000..de83f2aecc --- /dev/null +++ b/accessible/tests/mochitest/states/test_controls.xhtml @@ -0,0 +1,153 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL input control state tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + var gQueue = null; + function doTest() + { + testStates("checkbox", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE); + testStates("checkbox2", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE); + testStates("radiogroup", 0, 0, STATE_FOCUSABLE | STATE_UNAVAILABLE); + testStates("radio", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE); + testStates("radio-disabled", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE); + testStates("radiogroup-disabled", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE); + testStates("radio-disabledradiogroup", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE); + testStates("button", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE); + testStates("button-disabled", STATE_UNAVAILABLE, 0 , STATE_FOCUSABLE); + testStates("checkbutton", + STATE_FOCUSABLE | STATE_CHECKABLE | STATE_PRESSED); + testStates("fakecheckbutton", STATE_FOCUSABLE | STATE_PRESSED, 0, + STATE_CHECKABLE); + testStates("combobox", STATE_FOCUSABLE | STATE_HASPOPUP, 0, STATE_UNAVAILABLE); + testStates("combobox-disabled", STATE_UNAVAILABLE | STATE_HASPOPUP, 0, STATE_FOCUSABLE); + testStates("listbox", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE); + testStates("listitem", STATE_FOCUSABLE | STATE_SELECTABLE | STATE_SELECTED, 0, STATE_UNAVAILABLE); + testStates("listbox-disabled", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE | STATE_SELECTABLE); + testStates("listitem-disabledlistbox", STATE_UNAVAILABLE | STATE_SELECTED, 0, STATE_FOCUSABLE | STATE_SELECTABLE); + testStates("menubar", 0, 0, STATE_FOCUSABLE); + testStates("menu", STATE_FOCUSABLE, 0, STATE_UNAVAILABLE); + testStates("menu-disabled", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE | STATE_SELECTABLE); + testStates("tab", STATE_FOCUSABLE | STATE_SELECTABLE | STATE_SELECTED, 0, STATE_UNAVAILABLE); + testStates("tab-disabled", STATE_UNAVAILABLE, 0, STATE_FOCUSABLE | STATE_SELECTABLE | STATE_SELECTED); + + gQueue = new eventQueue(); + gQueue.invoke(); // Will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=599163" + title="check disabled state instead of attribute"> + Mozilla Bug 599163 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=756983" + title="Isolate focusable and unavailable states from State()"> + Mozilla Bug 756983 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + + <checkbox id="checkbox" checked="true" label="Steak"/> + <checkbox id="checkbox2" checked="true" label="Salad" disabled="true"/> + + <radiogroup id="radiogroup"> + <radio id="radio" label="Orange"/> + <radio id="radio-disabled" selected="true" label="Violet" disabled="true"/> + </radiogroup> + + <radiogroup id="radiogroup-disabled" disabled="true"> + <radio id="radio-disabledradiogroup" label="Orange"/> + <radio id="violet2" selected="true" label="Violet"/> + </radiogroup> + + <button id="button" value="button"/> + <button id="button-disabled" disabled="true" value="button"/> + <button id="checkbutton" type="checkbox" checked="true" + value="checkbutton" /> + <button id="fakecheckbutton" checked="true" value="fakecheckbutton" /> + + <menulist id="combobox"> + <menupopup> + <menuitem label="item1"/> + </menupopup> + </menulist> + + <menulist id="combobox-disabled" disabled="true"> + <menupopup> + <menuitem label="item1"/> + </menupopup> + </menulist> + + <richlistbox id="listbox"> + <richlistitem id="listitem"> + <label value="list item"/> + </richlistitem> + </richlistbox> + + <richlistbox id="listbox-disabled" disabled="true"> + <richlistitem id="listitem-disabledlistbox"> + <label value="list item"/> + </richlistitem> + </richlistbox> + + <toolbox> + <menubar id="menubar"> + <menu id="menu" label="menu1"> + <menupopup> + <menuitem id="menu1-item1" label="menuitem1.1"/> + </menupopup> + </menu> + <menu id="menu-disabled" label="menu2" disabled="true"> + <menupopup> + <menuitem id="menu-disabled-item1" label="menuitem2.1"/> + </menupopup> + </menu> + </menubar> + </toolbox> + + <tabbox> + <tabs> + <tab id="tab" label="tab1" tooltip="tooltip"/> + <tab id="tab-disabled" label="tab1" disabled="true"/> + </tabs> + <tabpanels> + <tabpanel/> + <tabpanel/> + </tabpanels> + </tabbox> + + <tooltip id="tooltip"><description>tooltip</description></tooltip> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/states/test_doc.html b/accessible/tests/mochitest/states/test_doc.html new file mode 100644 index 0000000000..a00ca957b9 --- /dev/null +++ b/accessible/tests/mochitest/states/test_doc.html @@ -0,0 +1,95 @@ +<!DOCTYPE html> +<html> +<head> + <title>states of document</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function doTest() { + // Bug 566542: root accesible should expose active state when focused. + testStates(getRootAccessible(), 0, EXT_STATE_ACTIVE); + + // Bug 509696, 607219. + testStates(document, STATE_READONLY, 0); // role="" + + document.body.setAttribute("role", "application"); + testStates(document, 0, 0, STATE_READONLY); + document.body.setAttribute("role", "foo"); // bogus role + testStates(document, STATE_READONLY); + document.body.removeAttribute("role"); + testStates(document, STATE_READONLY); + + // Bugs 454997 and 467387 + testStates(document, STATE_READONLY); + testStates("document", STATE_READONLY); + testStates("editable_document", 0, EXT_STATE_EDITABLE, STATE_READONLY); + + testStates("p", 0, EXT_STATE_SELECTABLE_TEXT, 0, EXT_STATE_EDITABLE); + testStates("unselectable_p", 0, 0, 0, EXT_STATE_SELECTABLE_TEXT | EXT_STATE_EDITABLE); + testStates("unselectable_link", 0, 0, 0, EXT_STATE_SELECTABLE_TEXT | EXT_STATE_EDITABLE); + + document.designMode = "on"; + + testStates(document, 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("p", 0, EXT_STATE_EDITABLE | EXT_STATE_SELECTABLE_TEXT, STATE_READONLY); + testStates("document", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("editable_document", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("unselectable_p", 0, EXT_STATE_SELECTABLE_TEXT | EXT_STATE_EDITABLE); + testStates("unselectable_link", 0, EXT_STATE_SELECTABLE_TEXT | EXT_STATE_EDITABLE); + + document.designMode = "off"; + + testStates(document, STATE_READONLY); + testStates("document", STATE_READONLY); + testStates("editable_document", 0, EXT_STATE_EDITABLE, STATE_READONLY); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body role=""> + + <a target="_blank" + title="<body contenteditable='true'> exposed incorrectly" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=454997">Mozilla Bug 454997</a> + <a target="_blank" + title="nsIAccessible states tests of editable document" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=467387">Mozilla Bug 467387</a> + <a target="_blank" + title="Role attribute on body with empty string causes DocAccessible not to have read-only state." + href="https://bugzilla.mozilla.org/show_bug.cgi?id=509696">Mozilla Bug 509696</a> + <a target="_blank" + title="Frame for firefox does not implement the state "active" when firefox is the active frame" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=566542">Mozilla Bug 566542</a> + <a target="_blank" + title="Dynamic role attribute change on body doesn't affect on document role" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=607219">Mozilla Bug 607219</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="p">hello</p> + + <p id="unselectable_p" style="user-select: none;">unselectable <a id="unselectable_link" href="#">link</a></p> + + <div id="document" role="document">document</div> + <div id="editable_document" role="document" contentEditable="true">editable document</doc> + +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_doc_busy.html b/accessible/tests/mochitest/states/test_doc_busy.html new file mode 100644 index 0000000000..d110faeb04 --- /dev/null +++ b/accessible/tests/mochitest/states/test_doc_busy.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<html> +<head> + <title>states of document</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + const { BrowserTestUtils } = ChromeUtils.import( + "resource://testing-common/BrowserTestUtils.jsm"); + + function matchDocBusyChange(isBusy) { + return function(event) { + const scEvent = event.QueryInterface(nsIAccessibleStateChangeEvent); + return ( + event.DOMNode == document && + scEvent.state === STATE_BUSY && + scEvent.isEnabled === isBusy + ); + }; + } + + async function doTest() { + // Because of variable timing, there are two acceptable possibilities: + // 1. We get an event for busy and an event for not busy. + // 2. The two events get coalesced, so no events are fired. + // However, we fail this test if we get the first event but not the + // second. + let gotBusy = false; + let gotNotBusy = false; + // We never actually wait on this; we just check the booleans it sets. + // We still need to hold a reference so it doesn't get garbage collected + // before it does anything. + // eslint-disable-next-line no-unused-vars + const busyEvents = async function() { + await waitForEvent(EVENT_STATE_CHANGE, matchDocBusyChange(true)); + info("Got busy event"); + gotBusy = true; + await waitForEvent(EVENT_STATE_CHANGE, matchDocBusyChange(false)); + info("Got not-busy event"); + gotNotBusy = true; + }(); + + const downloadPromptOpened = BrowserTestUtils.domWindowOpened(null, + async win => { + info("Window opened, waiting for load event"); + await BrowserTestUtils.waitForEvent(win, "load"); + info("Window loaded, checking if download prompt"); + return win.location && + win.location.href == "chrome://mozapps/content/downloads/unknownContentType.xhtml"; + } + ); + + info("Clicking link to trigger download"); + synthesizeMouse(getNode("link"), 1, 1, {}); + info("Waiting for download prompt to open"); + const downloadWin = await downloadPromptOpened; + + // Any busy events should have been fired by the time the download + // prompt has opened. + if (gotBusy && gotNotBusy) { + ok(true, "Got both busy change and not-busy change"); + } else if (!gotBusy && !gotNotBusy) { + ok(true, "No busy events, coalesced"); + } else { + ok(false, "Got busy change but didn't get not-busy change!"); + } + testStates(document, 0, 0, STATE_BUSY, 0, "Document not busy"); + + // Clean up. + info("Closing download prompt"); + downloadWin.close(); + // We might still be waiting on busy events. Remove any pending observers. + for (let observer of Services.obs.enumerateObservers( + "accessible-event") + ) { + Services.obs.removeObserver(observer, "accessible-event"); + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" + title="Missing busy state change event when downloading files" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=446469">Bug 446469</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <a id="link" href="http://example.com/a11y/accessible/tests/mochitest/dumbfile.zip">a file</a> +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_docarticle.html b/accessible/tests/mochitest/states/test_docarticle.html new file mode 100644 index 0000000000..8e45d5ebd6 --- /dev/null +++ b/accessible/tests/mochitest/states/test_docarticle.html @@ -0,0 +1,78 @@ +<html> +<head> + <title>states of document article</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function doTest() { + var docAcc = getAccessible(document, [nsIAccessibleDocument]); + if (docAcc) { + testStates(docAcc, STATE_READONLY); + testStates("aria_article", STATE_READONLY); + testStates("editable_aria_article", 0, EXT_STATE_EDITABLE, + STATE_READONLY); + testStates("article", STATE_READONLY); + testStates("editable_article", 0, EXT_STATE_EDITABLE, STATE_READONLY); + + document.designMode = "on"; + + testStates(docAcc, 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("aria_article", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("editable_aria_article", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("article", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("editable_article", 0, EXT_STATE_EDITABLE, STATE_READONLY); + + document.designMode = "off"; + + testStates(docAcc, STATE_READONLY); + testStates("aria_article", STATE_READONLY); + testStates("editable_aria_article", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("article", STATE_READONLY); + testStates("editable_article", 0, EXT_STATE_EDITABLE, STATE_READONLY); + } + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body role="article"> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=467387" + title="Expose non-editable documents as readonly, regardless of role"> + Mozilla Bug 467387 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=613502" + title="Map <article> like we do aria role article"> + Mozilla Bug 613502 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="aria_article" role="article">aria article</div> + <div id="editable_aria_article" role="article" contentEditable="true"> + editable aria article</div> + + <article id="article">article</article> + <article id="editable_article" contentEditable="true"> + editable article</article> + +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_editablebody.html b/accessible/tests/mochitest/states/test_editablebody.html new file mode 100644 index 0000000000..27b7201a2b --- /dev/null +++ b/accessible/tests/mochitest/states/test_editablebody.html @@ -0,0 +1,44 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=454997 +--> +<head> + <title>nsIAccessible states tests of contenteditable body</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function doTest() { + testStates(document, 0, EXT_STATE_EDITABLE); + testStates("p", 0, EXT_STATE_EDITABLE); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body contentEditable="true"> + + <a target="_blank" + title="nsIAccessible states tests of contenteditable body" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=454997">Mozilla Bug 454997</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="p">hello</p> +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_expandable.xhtml b/accessible/tests/mochitest/states/test_expandable.xhtml new file mode 100644 index 0000000000..0d969bef5b --- /dev/null +++ b/accessible/tests/mochitest/states/test_expandable.xhtml @@ -0,0 +1,112 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<!-- Firefox searchbar --> +<?xml-stylesheet href="chrome://browser/content/browser.css" + type="text/css"?> +<!-- SeaMonkey searchbar --> +<?xml-stylesheet href="chrome://navigator/content/navigator.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Expanded state change events tests for comboboxes and autocompletes."> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + + <script type="application/javascript" + src="../autocomplete.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //gA11yEventDumpToConsole = true; // debuggin + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(); + + gQueue.push(new openCombobox("menulist")); + gQueue.push(new closeCombobox("menulist")); + + todo(false, "Autocompletes don't fire expanded state change events when popup open. See bug 688480!"); + //gQueue.push(new openCombobox("autocomplete")); + //gQueue.push(new closeCombobox("autocomplete")); + + // XXX: searchbar doesn't fire state change events because accessible + // parent of combobox_list accessible is pushbutton accessible. + //var searchbar = document.getElementById("searchbar"); + //gQueue.push(new openHideCombobox(searchbar, true)); + //gQueue.push(new openHideCombobox(searchbar, false)); + todo(false, "Enable states test for XUL searchbar widget!"); + + gQueue.onFinish = function() + { + // unregister 'test-a11y-search' autocomplete search + shutdownAutoComplete(); + } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + // This is the hacks needed to use a searchbar without browser.js. + var BrowserSearch = { + updateOpenSearchBadge() {} + }; + + SimpleTest.waitForExplicitFinish(); + + // Register 'test-a11y-search' autocomplete search. + // XPFE AutoComplete needs to register early. + initAutoComplete([ "hello", "hi" ], + [ "Beep beep'm beep beep yeah", "Baby you can drive my car" ]); + + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox style="overflow: auto;" flex="1"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=467057" + title="xul menulist doesn't fire expand/collapse state change events"> + Mozilla Bug 467057 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menulist id="menulist"> + <menupopup> + <menuitem label="item1"/> + <menuitem label="item2"/> + <menuitem label="item3"/> + </menupopup> + </menulist> + + <html:input is="autocomplete-input" + id="autocomplete" + autocompletesearch="test-a11y-search"/> + + <searchbar id="searchbar"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/states/test_frames.html b/accessible/tests/mochitest/states/test_frames.html new file mode 100644 index 0000000000..baac222d83 --- /dev/null +++ b/accessible/tests/mochitest/states/test_frames.html @@ -0,0 +1,93 @@ +<html> + +<head> + <title>frame based document testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + if (navigator.platform.startsWith("Win")) { + SimpleTest.expectAssertions(0, 2); + } + + function doTest() { + const frameDoc = document.getElementById("frame_doc").contentDocument; + const frameDocArticle = document.getElementById("frame_doc_article").contentDocument; + const frameDocCheckbox = document.getElementById("frame_doc_checkbox").contentDocument; + const frameDocTextbox = document.getElementById("frame_doc_textbox").contentDocument; + + testStates(frameDoc, STATE_READONLY, 0, 0, 0, + "test1: frameDoc"); + testStates(frameDocArticle, STATE_READONLY, 0, 0, 0, + "test1: frameDocArticle"); + testStates(frameDocCheckbox, STATE_READONLY, 0, 0, 0, + "test1: frameDocCheckbox"); + testStates(frameDocTextbox, STATE_READONLY, 0, 0, 0, + "test1: frameDocTextbox"); + frameDoc.designMode = "on"; + testStates(frameDoc, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0, + "test2: frameDoc"); + testStates(frameDocArticle, STATE_READONLY, 0, 0, 0, + "test2: frameDocArticle"); + testStates(frameDocCheckbox, STATE_READONLY, 0, 0, 0, + "test2: frameDocCheckbox"); + testStates(frameDocTextbox, STATE_READONLY, 0, 0, 0, + "test2: frameDocTextbox"); + + frameDocArticle.designMode = "on"; + testStates(frameDocArticle, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0, + "test3: frameDocArticle"); + + frameDocCheckbox.designMode = "on"; + testStates(frameDocCheckbox, 0, EXT_STATE_EDITABLE, STATE_READONLY, 0, + "test4: frameDocCheckbox"); + + // Replace iframe document body before the document accessible tree is + // created. Check the states are updated for new body. + var frameUpdateDoc = + document.getElementById("frame_updatedoc").contentDocument; + testStates(frameUpdateDoc, 0, EXT_STATE_EDITABLE, + STATE_READONLY, EXT_STATE_STALE, "test5: frameUpdateDoc"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=467387" + title="Expose non-editable documents as readonly, regardless of role"> + Mozilla Bug 467387 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=638106" + title="CKEditor document should be editable"> + Mozilla Bug 638106 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <iframe id="frame_doc" src="z_frames.html"></iframe> + <iframe id="frame_doc_article" src="z_frames_article.html"></iframe> + <iframe id="frame_doc_checkbox" src="z_frames_checkbox.html"></iframe> + <iframe id="frame_doc_textbox" src="z_frames_textbox.html"></iframe> + <iframe id="frame_updatedoc" src="z_frames_update.html"></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_inputs.html b/accessible/tests/mochitest/states/test_inputs.html new file mode 100644 index 0000000000..d9b9014a34 --- /dev/null +++ b/accessible/tests/mochitest/states/test_inputs.html @@ -0,0 +1,268 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML input states</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function doTest() { + // ////////////////////////////////////////////////////////////////////////// + // 'editable' and 'multiline' states. + testStates("input", 0, EXT_STATE_EDITABLE, 0, EXT_STATE_MULTI_LINE); + testStates("textarea", 0, EXT_STATE_EDITABLE | EXT_STATE_MULTI_LINE); + + testStates("input_readonly", 0, EXT_STATE_EDITABLE); + testStates("input_disabled", 0, EXT_STATE_EDITABLE); + testStates("textarea_readonly", 0, EXT_STATE_EDITABLE); + testStates("textarea_disabled", 0, EXT_STATE_EDITABLE); + + // ////////////////////////////////////////////////////////////////////////// + // 'required', 'readonly' and 'unavailable' states. + var maybe_required = ["input", "search", "radio", "checkbox", "textarea"]; + var never_required = ["submit", "button", "reset", "image"]; + + var i; + for (i in maybe_required) { + testStates(maybe_required[i], + STATE_FOCUSABLE, 0, + STATE_REQUIRED | STATE_READONLY | STATE_UNAVAILABLE); + + testStates(maybe_required[i] + "_required", + STATE_FOCUSABLE | STATE_REQUIRED, 0, + STATE_UNAVAILABLE | STATE_READONLY); + + var readonlyID = maybe_required[i] + "_readonly"; + if (document.getElementById(readonlyID)) { + testStates(readonlyID, + STATE_FOCUSABLE | STATE_READONLY, 0, + STATE_UNAVAILABLE | STATE_REQUIRED); + } + + testStates(maybe_required[i] + "_disabled", + STATE_UNAVAILABLE, 0, + STATE_FOCUSABLE | STATE_READONLY | STATE_REQUIRED); + } + + for (i in never_required) { + testStates(never_required[i], 0, 0, STATE_REQUIRED | EXT_STATE_EDITABLE); + } + + // ////////////////////////////////////////////////////////////////////////// + // inherited 'unavailable' state + testStates("f", STATE_UNAVAILABLE); + testStates("f_input", STATE_UNAVAILABLE); + testStates("f_input_disabled", STATE_UNAVAILABLE); + + // ////////////////////////////////////////////////////////////////////////// + // inherited from file control + var fileBrowseButton = getAccessible("file").firstChild; + testStates(fileBrowseButton, STATE_UNAVAILABLE | STATE_REQUIRED); + // No states on the label. + + // ////////////////////////////////////////////////////////////////////////// + // 'invalid' state + var invalid = ["pattern", "email", "url"]; + for (i in invalid) { + testStates(invalid[i], STATE_INVALID); + testStates(invalid[i] + "2", 0, 0, STATE_INVALID); + } + + // ////////////////////////////////////////////////////////////////////////// + // not 'invalid' state + // (per spec, min/maxlength are always valid until interactively edited) + var validInput = document.createElement("input"); + validInput.maxLength = "0"; + validInput.value = "a"; + ok(validInput.validity.valid, + "input should be valid despite maxlength (no interactive edits)"); + + var validInput2 = document.createElement("input"); + validInput2.minLength = "1"; + validInput2.value = ""; + ok(validInput2.validity.valid, + "input should be valid despite minlength (no interactive edits)"); + + var valid = ["minlength", "maxlength"]; + for (i in valid) { + testStates(valid[i], 0, 0, STATE_INVALID); + testStates(valid[i] + "2", 0, 0, STATE_INVALID); + } + + // ////////////////////////////////////////////////////////////////////////// + // 'invalid' state + // (per spec, min/maxlength validity is affected by interactive edits) + var mininp = document.getElementById("minlength"); + mininp.focus(); + mininp.setSelectionRange(mininp.value.length, mininp.value.length); + synthesizeKey("KEY_Backspace"); + ok(!mininp.validity.valid, + "input should be invalid after interactive edits"); + testStates(mininp, STATE_INVALID); + // inputs currently cannot be made longer than maxlength interactively, + // so we're not testing that case. + + // ////////////////////////////////////////////////////////////////////////// + // autocomplete states + testStates("autocomplete-default", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("autocomplete-off", 0, 0, 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("autocomplete-formoff", 0, 0, 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("autocomplete-list", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("autocomplete-list2", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("autocomplete-tel", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("autocomplete-email", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + testStates("autocomplete-search", 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION); + + // ////////////////////////////////////////////////////////////////////////// + // haspopup + testStates("autocomplete-list", STATE_HASPOPUP); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559275" + title="map attribute required to STATE_REQUIRED"> + Bug 559275 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=389238" + title="Support disabled state on fieldset"> + Bug 389238 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=599163" + title="check disabled state instead of attribute"> + Bug 599163 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=601205" + title="Expose intrinsic invalid state to accessibility API"> + Bug 601205 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=601205" + title="Expose intrinsic invalid state to accessibility API"> + Bug 601205 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559766" + title="Add accessibility support for @list on HTML input and for HTML datalist"> + Bug 559766 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017" + title="File input control should be propogate states to descendants"> + Bug 699017 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=733382" + title="Editable state bit should be present on readonly inputs"> + Bug 733382 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=878590" + title="HTML5 datalist is not conveyed by haspopup property"> + Bug 878590 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + + <form> + <input id="input" type="input"> + <input id="input_required" type="input" required> + <input id="input_readonly" type="input" readonly> + <input id="input_disabled" type="input" disabled> + <input id="search" type="search"> + <input id="search_required" type="search" required> + <input id="search_readonly" type="search" readonly> + <input id="search_disabled" type="search" disabled> + <input id="radio" type="radio"> + <input id="radio_required" type="radio" required> + <input id="radio_disabled" type="radio" disabled> + <input id="checkbox" type="checkbox"> + <input id="checkbox_required" type="checkbox" required> + <input id="checkbox_disabled" type="checkbox" disabled> + <textarea id="textarea"></textarea> + <textarea id="textarea_required" required></textarea> + <textarea id="textarea_readonly" readonly></textarea> + <textarea id="textarea_disabled" disabled></textarea> + </form> + + <!-- bogus required usage --> + <input id="submit" type="submit" required> + <input id="button" type="button" required> + <input id="reset" type="reset" required> + <input id="image" type="image" required> + + <!-- inherited disabled --> + <fieldset id="f" disabled> + <input id="f_input"> + <input id="f_input_disabled" disabled> + </fieldset> + + <!-- inherited from input@type="file" --> + <input id="file" type="file" required disabled> + + <!-- invalid/valid --> + <input id="maxlength" maxlength="1" value="f"> + <input id="maxlength2" maxlength="100" value="foo"> + <input id="minlength" minlength="2" value="fo"> + <input id="minlength2" minlength="1" value="foo"> + <input id="pattern" pattern="bar" value="foo"> + <input id="pattern2" pattern="bar" value="bar"> + <input id="email" type="email" value="foo"> + <input id="email2" type="email" value="foo@bar.com"> + <input id="url" type="url" value="foo"> + <input id="url2" type="url" value="http://mozilla.org/"> + + <!-- autocomplete --> + <input id="autocomplete-default"> + <input id="autocomplete-off" autocomplete="off"> + <form autocomplete="off"> + <input id="autocomplete-formoff"> + </form> + <datalist id="cities"> + <option>Paris</option> + <option>San Francisco</option> + </datalist> + <input id="autocomplete-list" list="cities"> + <input id="autocomplete-list2" list="cities" autocomplete="off"> + <input id="autocomplete-tel" type="tel"> + + Email Address: + <input id="autocomplete-email" type="email" list="contacts" value="xyzzy"> + <datalist id="contacts"> + <option>xyzzy@plughs.com</option> + <option>nobody@mozilla.org</option> + </datalist> + + </br>Search for: + <input id="autocomplete-search" type="search" list="searchhisty" value="Gamma"> + <datalist id="searchhisty"> + <option>Gamma Rays</option> + <option>Gamma Ray Bursts</option> + </datalist> + + </body> +</html> diff --git a/accessible/tests/mochitest/states/test_link.html b/accessible/tests/mochitest/states/test_link.html new file mode 100644 index 0000000000..65632bd12f --- /dev/null +++ b/accessible/tests/mochitest/states/test_link.html @@ -0,0 +1,85 @@ +<html> + +<head> + <title>HTML link states testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function doTest() { + // a@href and its text node + testStates("link_href", STATE_LINKED); + testStates(getAccessible("link_href").firstChild, STATE_LINKED); + + // a@onclick + testStates("link_click", STATE_LINKED); + + // a@onmousedown + testStates("link_mousedown", STATE_LINKED); + + // a@onmouseup + testStates("link_mouseup", STATE_LINKED); + + // a@role="link" + testStates("link_arialink", STATE_LINKED); + + // a@role="button" + testStates("link_ariabutton", 0, 0, STATE_LINKED); + + // a (no @href, no click event listener) + testStates("link_notlink", 0, 0, STATE_LINKED); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=423409" + title="Expose click action if mouseup and mousedown are registered"> + Mozilla Bug 423409 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=754830" + title="Calculate link states separately"> + Mozilla Bug 754830 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=757774" + title="Fire state change event when link is traversed"> + Mozilla Bug 757774 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <a id="link_href" href="http://mozilla.org">link</a> + <a id="link_click" onclick="">link</a> + <a id="link_mousedown" onmousedown="">link</a> + <a id="link_mouseup" onmouseup="">link</a> + <a id="link_arialink" role="link">aria link</a> + <a id="link_ariabutton" role="button">aria button</a> + <a id="link_notlink">not link</a> + +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_popup.xhtml b/accessible/tests/mochitest/states/test_popup.xhtml new file mode 100644 index 0000000000..c51e3ac17c --- /dev/null +++ b/accessible/tests/mochitest/states/test_popup.xhtml @@ -0,0 +1,54 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL popup attribute test"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + + <script type="application/javascript"> + <![CDATA[ + function doTest() + { + // label with popup + testStates("labelWithPopup", STATE_HASPOPUP); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=504252" + title="Expose STATE_HASPOPUP on XUL elements that have an @popup attribute"> + Mozilla Bug 504252 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <!-- label with popup attribute --> + <label id="labelWithPopup" value="file name" + popup="fileContext" + tabindex="0"/> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/states/test_selects.html b/accessible/tests/mochitest/states/test_selects.html new file mode 100644 index 0000000000..37ff568458 --- /dev/null +++ b/accessible/tests/mochitest/states/test_selects.html @@ -0,0 +1,196 @@ +<html> + +<head> + <title>HTML selects accessible states tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function openComboboxNCheckStates(aID) { + this.combobox = getAccessible(aID); + this.comboboxList = this.combobox.firstChild; + this.comboboxOption = this.comboboxList.firstChild; + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, this.comboboxOption), + ]; + + this.invoke = function openComboboxNCheckStates_invoke() { + getNode(aID).focus(); + synthesizeKey("VK_DOWN", { altKey: true }); + }; + + this.finalCheck = function openComboboxNCheckStates_invoke() { + // Expanded state on combobox. + testStates(this.combobox, STATE_EXPANDED); + + // Floating state on combobox list. + testStates(this.comboboxList, STATE_FLOATING); + }; + + this.getID = function openComboboxNCheckStates_getID() { + return "open combobox and test states"; + }; + } + + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTest() { + // combobox + var combobox = getAccessible("combobox"); + testStates(combobox, + STATE_HASPOPUP | STATE_COLLAPSED | STATE_FOCUSABLE, 0, + STATE_FOCUSED, 0); + + var comboboxList = combobox.firstChild; + testStates(comboboxList, STATE_INVISIBLE, 0, STATE_FOCUSABLE, 0); + + var opt1 = comboboxList.firstChild; + testStates(opt1, STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE, + EXT_STATE_ACTIVE, STATE_FOCUSED, 0); + + var opt2 = comboboxList.lastChild; + testStates(opt2, STATE_SELECTABLE | STATE_FOCUSABLE, 0, STATE_SELECTED, 0, + STATE_FOCUSED, EXT_STATE_ACTIVE); + + // collapsed combobox + testStates("collapsedcombobox", + STATE_COLLAPSED | STATE_FOCUSABLE, 0, + STATE_FOCUSED, 0); + + testStates("collapsed-1", + STATE_FOCUSABLE | STATE_SELECTABLE, 0, + STATE_OFFSCREEN | STATE_INVISIBLE, 0); + + testStates("collapsed-2", + STATE_OFFSCREEN, 0, + STATE_INVISIBLE, 0); + + // listbox + testStates("listbox", + STATE_FOCUSABLE, 0, + STATE_HASPOPUP | STATE_COLLAPSED | STATE_FOCUSED); + + testStates("listitem-active", + STATE_FOCUSABLE | STATE_SELECTABLE, EXT_STATE_ACTIVE, + STATE_SELECTED | STATE_FOCUSED); + + testStates("listitem", + STATE_FOCUSABLE | STATE_SELECTABLE, 0, + STATE_SELECTED | STATE_FOCUSED, EXT_STATE_ACTIVE); + + testStates("listitem-disabled", + STATE_UNAVAILABLE, 0, + STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE, + EXT_STATE_ACTIVE); + + testStates("listgroup", + 0, 0, + STATE_UNAVAILABLE | STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE, + EXT_STATE_ACTIVE); + + testStates("listgroup-disabled", + STATE_UNAVAILABLE, 0, + STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE, + EXT_STATE_ACTIVE); + + todo(false, "no unavailable state on option in disabled group (bug 759666)"); +// testStates("listitem-disabledgroup", +// STATE_UNAVAILABLE, 0, +// STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE, +// EXT_STATE_ACTIVE); + + testStates("listbox-disabled", + STATE_UNAVAILABLE, 0, + STATE_FOCUSABLE); + + todo(false, "no unavailable state on option in disabled select (bug 759666)"); +// testStates("listitem-disabledlistbox", +// STATE_UNAVAILABLE, 0, +// STATE_SELECTABLE | STATE_SELECTED | STATE_FOCUSABLE, +// EXT_STATE_ACTIVE); + + // open combobox + gQueue = new eventQueue(); + gQueue.push(new openComboboxNCheckStates("combobox")); + gQueue.invoke(); // Will call */SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=443889" + title="mochitest for selects and lists"> + Mozilla Bug 443889 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=640716" + title="mochitest for selects and lists"> + Mozilla Bug 640716 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=689847" + title="Expose active state on current item of selectable widgets"> + Mozilla Bug 689847 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=756983" + title="Isolate focusable and unavailable states from State()"> + Mozilla Bug 756983 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=907682" + title=" HTML:option group position is not correct when select is collapsed"> + Mozilla Bug 907682 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="combobox"> + <option>item 1</option> + <option>item 2</option> + </select> + + <select id="collapsedcombobox"> + <option id="collapsed-1">item 1</option> + <option id="collapsed-2">item 2</option> + </select> + + <select id="listbox" name="component" size="3"> + <option id="listitem-active">Build</option> + <option id="listitem">Disability Access APIs</option> + <option id="listitem-disabled" disabled>General</option> + <optgroup id="listgroup" label="group"> + <option>option</option> + </optgroup> + <optgroup id="listgroup-disabled" disabled label="group2"> + <option id="listitem-disabledgroup">UI</option> + </optgroup> + </select> + + <select id="listbox-disabled" size="3" disabled> + <option id="listitem-disabledlistbox">option</option> + </select> + +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_stale.html b/accessible/tests/mochitest/states/test_stale.html new file mode 100644 index 0000000000..3218a2125a --- /dev/null +++ b/accessible/tests/mochitest/states/test_stale.html @@ -0,0 +1,108 @@ +<!DOCTYPE html> +<html> +<head> + <title>Stale state testing</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function addChild(aContainerID) { + this.containerNode = getNode(aContainerID); + this.childNode = null; + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function addChild_invoke() { + this.childNode = document.createElement("div"); + // Note after bug 646216, a sole div without text won't be accessible + // and would not result in an embedded character. + // Therefore, add some text. + this.childNode.textContent = "hello"; + this.containerNode.appendChild(this.childNode); + }; + + this.finalCheck = function addChild_finalCheck() { + // no stale state should be set + testStates(this.childNode, 0, 0, 0, EXT_STATE_STALE); + }; + + this.getID = function addChild_getID() { + return "add child for " + prettyName(aContainerID); + }; + } + + function removeChildChecker(aInvoker) { + this.type = EVENT_HIDE; + this.__defineGetter__("target", function() { return aInvoker.child; }); + + this.check = function removeChildChecker_check() { + // stale state should be set + testStates(aInvoker.child, 0, EXT_STATE_STALE); + }; + } + + function removeChild(aContainerID) { + this.containerNode = getNode(aContainerID); + this.child = null; + + this.eventSeq = [ + new removeChildChecker(this), + ]; + + this.invoke = function removeChild_invoke() { + var childNode = this.containerNode.firstChild; + this.child = getAccessible(childNode); + + this.containerNode.removeChild(childNode); + }; + + this.getID = function removeChild_getID() { + return "remove child from " + prettyName(aContainerID); + }; + } + + // gA11yEventDumpToConsole = true; //debugging + + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + + gQueue.push(new addChild("container")); + gQueue.push(new removeChild("container")); + + gQueue.invoke(); // will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body role=""> + + <a target="_blank" + title="Expose stale state on accessibles unattached from tree" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=676267">Mozilla Bug 676267</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="container"></div> + +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_tabs.xhtml b/accessible/tests/mochitest/states/test_tabs.xhtml new file mode 100644 index 0000000000..4d8a83c97d --- /dev/null +++ b/accessible/tests/mochitest/states/test_tabs.xhtml @@ -0,0 +1,66 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tabbox hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + testStates("tab1", 0, EXT_STATE_PINNED); + testStates("tab2", 0, 0, 0, EXT_STATE_PINNED); + testStates("tab3", 0, 0, 0, EXT_STATE_PINNED); + + SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=577727" + title="Make pinned tabs distinguishable from other tabs for accessibility"> + Mozilla Bug 577727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tabbox> + <tabs id="tabs"> + <tab id="tab1" label="tab1" pinned="true"/> + <tab id="tab2" label="tab2" pinned="false"/> + <tab id="tab3" label="tab3"/> + </tabs> + <tabpanels id="tabpanels"> + <tabpanel/> + <tabpanel/> + </tabpanels> + </tabbox> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/states/test_textbox.xhtml b/accessible/tests/mochitest/states/test_textbox.xhtml new file mode 100644 index 0000000000..764c1ad01c --- /dev/null +++ b/accessible/tests/mochitest/states/test_textbox.xhtml @@ -0,0 +1,78 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="nsIAccessible XUL textboxes states tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + + <script type="application/javascript"> + <![CDATA[ + function getInput(aID) + { + return getNode(aID).inputField; + } + + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // Search textbox without search button, searches as you type and filters + // a separate control. + testStates(getInput("searchbox"), + STATE_FOCUSABLE, + EXT_STATE_EDITABLE | EXT_STATE_SUPPORTS_AUTOCOMPLETION, + STATE_PROTECTED | STATE_UNAVAILABLE, + 0, + "searchbox"); + + ////////////////////////////////////////////////////////////////////////// + // Search textbox with search button, does not support autoCompletion. + testStates(getInput("searchfield"), + STATE_FOCUSABLE, + EXT_STATE_EDITABLE, + STATE_PROTECTED | STATE_UNAVAILABLE, + 0, + "searchfield"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=442648"> + Mozilla Bug 442648 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=648235" + title="XUL textbox can inherit more states from underlying HTML input"> + Mozilla Bug 648235 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <search-textbox id="searchbox" flex="1" results="historyTree"/> + <search-textbox id="searchfield" placeholder="Search all add-ons" + searchbutton="true"/> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/states/test_tree.xhtml b/accessible/tests/mochitest/states/test_tree.xhtml new file mode 100644 index 0000000000..d81be4a54b --- /dev/null +++ b/accessible/tests/mochitest/states/test_tree.xhtml @@ -0,0 +1,146 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> +<?xml-stylesheet href="../treeview.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tree states tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + /** + * Event queue invoker object to test accessible states for XUL tree + * accessible. + */ + function statesChecker(aTreeID, aView) + { + this.DOMNode = getNode(aTreeID); + + this.invoke = function statesChecker_invoke() + { + this.DOMNode.view = aView; + } + + this.check = function statesChecker_check() + { + var tree = getAccessible(this.DOMNode); + + // tree states + testStates(tree, STATE_READONLY); + + if (this.DOMNode.getAttribute("seltype") != "single") + testStates(tree, STATE_MULTISELECTABLE); + else + testStates(tree, 0, 0, STATE_MULTISELECTABLE); + + // tree item states + var expandedItem = tree.getChildAt(2); + testStates(expandedItem, + STATE_SELECTABLE | STATE_FOCUSABLE | STATE_EXPANDED); + + var collapsedItem = tree.getChildAt(5); + testStates(collapsedItem, + STATE_SELECTABLE | STATE_FOCUSABLE | STATE_COLLAPSED); + + // cells states if any + var cells = collapsedItem.children; + if (cells && cells.length) { + for (var idx = 0; idx < cells.length; idx++) { + var cell = cells.queryElementAt(idx, nsIAccessible); + testStates(cell, STATE_SELECTABLE); + } + + var checkboxCell = cells.queryElementAt(3, nsIAccessible); + testStates(checkboxCell, STATE_CHECKABLE | STATE_CHECKED); + } + } + + this.getID = function statesChecker_getID() + { + return "tree processor for " + prettyName(aTreeID); + } + } + + gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + + function doTest() + { + gQueue = new eventQueue(EVENT_REORDER); + gQueue.push(new statesChecker("tree", new nsTreeTreeView())); + gQueue.push(new statesChecker("treesingle", new nsTreeTreeView())); + gQueue.push(new statesChecker("tabletree", new nsTreeTreeView())); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Mozilla Bug 503727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <tree id="treesingle" flex="1" seltype="single"> + <treecols> + <treecol id="col_single" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <tree id="tabletree" flex="1" editable="true"> + <treecols> + <treecol id="tabletree_col1" cycler="true" label="cycler"/> + <treecol id="tabletree_col2" flex="1" primary="true" label="column1"/> + <treecol id="tabletree_col3" flex="1" label="column2"/> + <treecol id="tabletree_col4" flex="1" label="checker" + type="checkbox" editable="true"/> + </treecols> + <treechildren/> + </tree> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/states/test_visibility.html b/accessible/tests/mochitest/states/test_visibility.html new file mode 100644 index 0000000000..aa3643673a --- /dev/null +++ b/accessible/tests/mochitest/states/test_visibility.html @@ -0,0 +1,75 @@ +<html> +<head> + <title>visibility state testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // Tests + + function doTests() { + testStates("div", 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + testStates("div_off", STATE_OFFSCREEN, 0, STATE_INVISIBLE); + testStates("div_transformed", STATE_OFFSCREEN, 0, STATE_INVISIBLE); + testStates("div_abschild", 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + testStates("ul", STATE_OFFSCREEN, 0, STATE_INVISIBLE); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=591363" + title="(in)visible state is not always correct?"> + Mozilla Bug 591363 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=768786" + title="Offscreen state is not exposed under certain circumstances"> + Mozilla Bug 768786 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="outer_div"> + + <!-- trivial cases --> + <div id="div">div</div> + <div id="div_off" style="position: absolute; left:-999px; top:-999px"> + offscreen! + </div> + <div id="div_transformed" style="transform: translate(-999px, -999px);"> + transformed! + </div> + + <!-- edge case: no rect but has out of flow child --> + <div id="div_abschild"> + <p style="position: absolute; left: 120px; top:120px;">absolute</p> + </div> + + <ul id="ul" style="display: contents;"> + <li>Supermarket 1</li> + <li>Supermarket 2</li> + </ul> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/states/test_visibility.xhtml b/accessible/tests/mochitest/states/test_visibility.xhtml new file mode 100644 index 0000000000..28d8c995db --- /dev/null +++ b/accessible/tests/mochitest/states/test_visibility.xhtml @@ -0,0 +1,151 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="XUL elements visibility states testing"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + function openMenu(aID, aSubID, aOffscreenSubID) + { + this.menuNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, this.menuNode) + ]; + + this.invoke = function openMenu_invoke() + { + this.menuNode.open = true; + } + + this.finalCheck = function openMenu_finalCheck() + { + testStates(aID, 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + testStates(aSubID, 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + if (aOffscreenSubID) + testStates(aOffscreenSubID, STATE_OFFSCREEN, 0, STATE_INVISIBLE); + } + + this.getID = function openMenu_invoke() + { + return "open menu '" + aID + "' and test states"; + } + } + + function closeMenu(aID, aSubID, aSub2ID) + { + this.menuNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, document) + ]; + + this.invoke = function openMenu_invoke() + { + this.menuNode.open = false; + } + + this.finalCheck = function openMenu_finalCheck() + { + testStates(aID, 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + testStates(aSubID, STATE_INVISIBLE, 0, STATE_OFFSCREEN); + testStates(aSub2ID, STATE_INVISIBLE, 0, STATE_OFFSCREEN); + } + + this.getID = function openMenu_invoke() + { + return "open menu and test states"; + } + } + + var gQueue = null; + function doTest() + { + testStates("deck_pane2", 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + testStates("tabs_pane1", 0, 0, STATE_INVISIBLE | STATE_OFFSCREEN); + testStates("tabs_pane2", STATE_OFFSCREEN, 0, STATE_INVISIBLE); + + gQueue = new eventQueue(); + gQueue.push(new openMenu("mi_file1", "mi_file1.1")); + gQueue.push(new openMenu("mi_file1.2", "mi_file1.2.1", "mi_file1.2.4")); + gQueue.push(new closeMenu("mi_file1", "mi_file1.1", "mi_file1.2.1")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=810260" + title="xul:deck hidden pages shouldn't be offscreen"> + Mozilla Bug 810260 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=865591" + title="Visible menu item have offscreen state"> + Mozilla Bug 865591 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + + <deck selectedIndex="1"> + <description value="This is the first page" id="deck_pane1"/> + <button label="This is the second page" id="deck_pane2"/> + </deck> + + <tabbox> + <tabs> + <tab>tab1</tab> + <tab>tab2</tab> + </tabs> + <tabpanels> + <description value="This is the first page" id="tabs_pane1"/> + <button label="This is the second page" id="tabs_pane2"/> + </tabpanels> + </tabbox> + + <menubar> + <menu label="File" id="mi_file1"> + <menupopup> + <menuitem label="SubFile" id="mi_file1.1"/> + <menu label="SubFile2" id="mi_file1.2"> + <menupopup style="max-height: 5em;"> + <menuitem label="SubSubFile" id="mi_file1.2.1"/> + <menuitem label="SubSubFile2" id="mi_file1.2.2"/> + <menuitem label="SubSubFile3" id="mi_file1.2.3"/> + <menuitem label="SubSubFile4" id="mi_file1.2.4"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menubar> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/states/z_frames.html b/accessible/tests/mochitest/states/z_frames.html new file mode 100644 index 0000000000..819adee63e --- /dev/null +++ b/accessible/tests/mochitest/states/z_frames.html @@ -0,0 +1,11 @@ +<html> +<!-- +Auxilliary file used as frame source. +--> +<head> +</head> +<body> +<p>Frame source body has no role</p> +</body> +</html> + diff --git a/accessible/tests/mochitest/states/z_frames_article.html b/accessible/tests/mochitest/states/z_frames_article.html new file mode 100644 index 0000000000..a7a69b4dae --- /dev/null +++ b/accessible/tests/mochitest/states/z_frames_article.html @@ -0,0 +1,11 @@ +<html> +<!-- +Auxilliary file used as frame source. +--> +<head> +</head> +<body role="article"> +<p>Article</p> +</body> +</html> + diff --git a/accessible/tests/mochitest/states/z_frames_checkbox.html b/accessible/tests/mochitest/states/z_frames_checkbox.html new file mode 100644 index 0000000000..7997644243 --- /dev/null +++ b/accessible/tests/mochitest/states/z_frames_checkbox.html @@ -0,0 +1,11 @@ +<html> +<!-- +Auxilliary file used as frame source. +--> +<head> +</head> +<body role="checkbox"> +<p>Checkbox</p> +</body> +</html> + diff --git a/accessible/tests/mochitest/states/z_frames_textbox.html b/accessible/tests/mochitest/states/z_frames_textbox.html new file mode 100644 index 0000000000..0f4e1b9d66 --- /dev/null +++ b/accessible/tests/mochitest/states/z_frames_textbox.html @@ -0,0 +1,11 @@ +<html> +<!-- +Auxilliary file used as frame source. +--> +<head> +</head> +<body role="textbox"> +<p>Texbox</p> +</body> +</html> + diff --git a/accessible/tests/mochitest/states/z_frames_update.html b/accessible/tests/mochitest/states/z_frames_update.html new file mode 100644 index 0000000000..7e2cc83539 --- /dev/null +++ b/accessible/tests/mochitest/states/z_frames_update.html @@ -0,0 +1,21 @@ +<html> +<head> +<script> +function replaceBody() { + var accService = Cc["@mozilla.org/accessibilityService;1"]. + getService(Ci.nsIAccessibilityService); + accService.getAccessibleFor(document); + + var newBody = document.createElement("body"); + newBody.setAttribute("contentEditable", "true"); + newBody.textContent = "New Hello"; + document.documentElement.replaceChild(newBody, document.body); + getComputedStyle(newBody, "").color; +} +</script> +</head> +<body onload="replaceBody();"> +OLD hello +</body> +</html> + diff --git a/accessible/tests/mochitest/table.js b/accessible/tests/mochitest/table.js new file mode 100644 index 0000000000..83fdf1e207 --- /dev/null +++ b/accessible/tests/mochitest/table.js @@ -0,0 +1,1033 @@ +/** + * This file provides set of helper functions to test nsIAccessibleTable + * interface. + * + * Required: + * common.js + * role.js + * states.js + */ +/* import-globals-from common.js */ +/* import-globals-from role.js */ +/* import-globals-from states.js */ + +/** + * Constants used to describe cells array. + */ +const kDataCell = 1; // Indicates the cell is origin data cell +const kRowHeaderCell = 2; // Indicates the cell is row header cell +const kColHeaderCell = 4; // Indicated the cell is column header cell +const kOrigin = kDataCell | kRowHeaderCell | kColHeaderCell; + +const kRowSpanned = 8; // Indicates the cell is not origin and row spanned +const kColSpanned = 16; // Indicates the cell is not origin and column spanned +const kSpanned = kRowSpanned | kColSpanned; + +/** + * Constants to define column header type. + */ +const kNoColumnHeader = 0; +const kListboxColumnHeader = 1; +const kTreeColumnHeader = 2; + +/** + * Constants to define table type. + */ +const kTable = 0; +const kTreeTable = 1; +const kMathTable = 2; + +/** + * Test table structure and related methods. + * + * @param aIdentifier [in] table accessible identifier + * @param aCellsArray [in] two dimensional array (row X columns) of + * cell types (see constants defined above). + * @param aColHeaderType [in] specifies wether column header cells are + * arranged into the list. + * @param aCaption [in] caption text if any + * @param aSummary [in] summary text if any + * @param aTableType [in] specifies the table type. + * @param aRowRoles [in] array of row roles. + */ +function testTableStruct( + aIdentifier, + aCellsArray, + aColHeaderType, + aCaption, + aSummary, + aTableType, + aRowRoles +) { + var tableNode = getNode(aIdentifier); + var isGrid = + tableNode.getAttribute("role") == "grid" || + tableNode.getAttribute("role") == "treegrid" || + tableNode.localName == "tree"; + + var rowCount = aCellsArray.length; + var colsCount = aCellsArray[0] ? aCellsArray[0].length : 0; + + // Test table accessible tree. + var tableObj = { + children: [], + }; + switch (aTableType) { + case kTable: + tableObj.role = ROLE_TABLE; + break; + case kTreeTable: + tableObj.role = ROLE_TREE_TABLE; + break; + case kMathTable: + tableObj.role = ROLE_MATHML_TABLE; + break; + } + + // caption accessible handling + if (aCaption) { + var captionObj = { + role: ROLE_CAPTION, + children: [ + { + role: ROLE_TEXT_LEAF, + name: aCaption, + }, + ], + }; + + tableObj.children.push(captionObj); + } + + // special types of column headers handling + if (aColHeaderType) { + var headersObj = { + role: ROLE_LIST, + children: [], + }; + + for (let idx = 0; idx < colsCount; idx++) { + var headerCellObj = { + role: ROLE_COLUMNHEADER, + }; + headersObj.children.push(headerCellObj); + } + + if (aColHeaderType == kTreeColumnHeader) { + var columnPickerObj = { + role: ROLE_PUSHBUTTON, + }; + + headersObj.children.push(columnPickerObj); + } + + tableObj.children.push(headersObj); + } + + // rows and cells accessibles + for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) { + let rowObj = { + role: aRowRoles ? aRowRoles[rowIdx] : ROLE_ROW, + children: [], + }; + + for (let colIdx = 0; colIdx < colsCount; colIdx++) { + let celltype = aCellsArray[rowIdx][colIdx]; + + var role = ROLE_NOTHING; + switch (celltype) { + case kDataCell: + role = + aTableType == kMathTable + ? ROLE_MATHML_CELL + : isGrid + ? ROLE_GRID_CELL + : ROLE_CELL; + break; + case kRowHeaderCell: + role = ROLE_ROWHEADER; + break; + case kColHeaderCell: + role = ROLE_COLUMNHEADER; + break; + } + + if (role != ROLE_NOTHING) { + var cellObj = { role }; + rowObj.children.push(cellObj); + } + } + + tableObj.children.push(rowObj); + } + + testAccessibleTree(aIdentifier, tableObj); + + // Test table table interface. + var table = getAccessible(aIdentifier, [nsIAccessibleTable]); + + // summary + if (aSummary) { + is( + table.summary, + aSummary, + "Wrong summary of the table " + prettyName(aIdentifier) + ); + } + + // rowCount and columnCount + is( + table.rowCount, + rowCount, + "Wrong rows count of " + prettyName(aIdentifier) + ); + is( + table.columnCount, + colsCount, + "Wrong columns count of " + prettyName(aIdentifier) + ); + + // rows and columns extents + for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) { + for (let colIdx = 0; colIdx < colsCount; colIdx++) { + let celltype = aCellsArray[rowIdx][colIdx]; + if (celltype & kOrigin) { + // table getRowExtentAt + var rowExtent = table.getRowExtentAt(rowIdx, colIdx); + let idx; + /* eslint-disable no-empty */ + for ( + idx = rowIdx + 1; + idx < rowCount && aCellsArray[idx][colIdx] & kRowSpanned; + idx++ + ) {} + /* eslint-enable no-empty */ + + var expectedRowExtent = idx - rowIdx; + is( + rowExtent, + expectedRowExtent, + "getRowExtentAt: Wrong number of spanned rows at (" + + rowIdx + + ", " + + colIdx + + ") for " + + prettyName(aIdentifier) + ); + + // table getColumnExtentAt + var colExtent = table.getColumnExtentAt(rowIdx, colIdx); + /* eslint-disable no-empty */ + for ( + idx = colIdx + 1; + idx < colsCount && aCellsArray[rowIdx][idx] & kColSpanned; + idx++ + ) {} + /* eslint-enable no-empty */ + + var expectedColExtent = idx - colIdx; + is( + colExtent, + expectedColExtent, + "getColumnExtentAt: Wrong number of spanned columns at (" + + rowIdx + + ", " + + colIdx + + ") for " + + prettyName(aIdentifier) + ); + + // cell rowExtent and columnExtent + var cell = getAccessible(table.getCellAt(rowIdx, colIdx), [ + nsIAccessibleTableCell, + ]); + + is( + cell.rowExtent, + expectedRowExtent, + "rowExtent: Wrong number of spanned rows at (" + + rowIdx + + ", " + + colIdx + + ") for " + + prettyName(aIdentifier) + ); + + is( + cell.columnExtent, + expectedColExtent, + "columnExtent: Wrong number of spanned column at (" + + rowIdx + + ", " + + colIdx + + ") for " + + prettyName(aIdentifier) + ); + } + } + } +} + +/** + * Test table indexes. + * + * @param aIdentifier [in] table accessible identifier + * @param aIdxes [in] two dimensional array of cell indexes + */ +function testTableIndexes(aIdentifier, aIdxes) { + var tableAcc = getAccessible(aIdentifier, [nsIAccessibleTable]); + if (!tableAcc) { + return; + } + + var obtainedRowIdx, obtainedColIdx, obtainedIdx; + var cellAcc; + + var id = prettyName(aIdentifier); + + var rowCount = aIdxes.length; + for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) { + var colCount = aIdxes[rowIdx].length; + for (var colIdx = 0; colIdx < colCount; colIdx++) { + var idx = aIdxes[rowIdx][colIdx]; + + // getCellAt + try { + cellAcc = null; + cellAcc = tableAcc.getCellAt(rowIdx, colIdx); + } catch (e) {} + + ok( + (idx != -1 && cellAcc) || (idx == -1 && !cellAcc), + id + + ": Can't get cell accessible at row = " + + rowIdx + + ", column = " + + colIdx + ); + + if (idx != -1) { + // getRowIndexAt + var origRowIdx = rowIdx; + while ( + origRowIdx > 0 && + aIdxes[rowIdx][colIdx] == aIdxes[origRowIdx - 1][colIdx] + ) { + origRowIdx--; + } + + try { + obtainedRowIdx = tableAcc.getRowIndexAt(idx); + } catch (e) { + ok( + false, + id + ": can't get row index for cell index " + idx + "," + e + ); + } + + is( + obtainedRowIdx, + origRowIdx, + id + ": row for index " + idx + " is not correct (getRowIndexAt)" + ); + + // getColumnIndexAt + var origColIdx = colIdx; + while ( + origColIdx > 0 && + aIdxes[rowIdx][colIdx] == aIdxes[rowIdx][origColIdx - 1] + ) { + origColIdx--; + } + + try { + obtainedColIdx = tableAcc.getColumnIndexAt(idx); + } catch (e) { + ok( + false, + id + ": can't get column index for cell index " + idx + "," + e + ); + } + + is( + obtainedColIdx, + origColIdx, + id + + ": column for index " + + idx + + " is not correct (getColumnIndexAt)" + ); + + // getRowAndColumnIndicesAt + var obtainedRowIdxObj = {}, + obtainedColIdxObj = {}; + try { + tableAcc.getRowAndColumnIndicesAt( + idx, + obtainedRowIdxObj, + obtainedColIdxObj + ); + } catch (e) { + ok( + false, + id + + ": can't get row and column indices for cell index " + + idx + + "," + + e + ); + } + + is( + obtainedRowIdxObj.value, + origRowIdx, + id + + ": row for index " + + idx + + " is not correct (getRowAndColumnIndicesAt)" + ); + is( + obtainedColIdxObj.value, + origColIdx, + id + + ": column for index " + + idx + + " is not correct (getRowAndColumnIndicesAt)" + ); + + if (cellAcc) { + var cellId = prettyName(cellAcc); + cellAcc = getAccessible(cellAcc, [nsIAccessibleTableCell]); + + // cell: 'table-cell-index' attribute + var attrs = cellAcc.attributes; + var strIdx = ""; + try { + strIdx = attrs.getStringProperty("table-cell-index"); + } catch (e) { + ok( + false, + cellId + + ": no cell index from object attributes on the cell accessible at index " + + idx + + "." + ); + } + + if (strIdx) { + is( + parseInt(strIdx), + idx, + cellId + + ": cell index from object attributes of cell accessible isn't corrent." + ); + } + + // cell: table + try { + is( + cellAcc.table, + tableAcc, + cellId + ": wrong table accessible for the cell." + ); + } catch (e) { + ok(false, cellId + ": can't get table accessible from the cell."); + } + + // cell: getRowIndex + try { + obtainedRowIdx = cellAcc.rowIndex; + } catch (e) { + ok( + false, + cellId + + ": can't get row index of the cell at index " + + idx + + "," + + e + ); + } + + is( + obtainedRowIdx, + origRowIdx, + cellId + ": row for the cell at index " + idx + " is not correct" + ); + + // cell: getColumnIndex + try { + obtainedColIdx = cellAcc.columnIndex; + } catch (e) { + ok( + false, + cellId + + ": can't get column index of the cell at index " + + idx + + "," + + e + ); + } + + is( + obtainedColIdx, + origColIdx, + id + ": column for the cell at index " + idx + " is not correct" + ); + } + } + + // getCellIndexAt + try { + obtainedIdx = tableAcc.getCellIndexAt(rowIdx, colIdx); + } catch (e) { + obtainedIdx = -1; + } + + is( + obtainedIdx, + idx, + id + + ": row " + + rowIdx + + " /column " + + colIdx + + " and index " + + obtainedIdx + + " aren't inconsistent." + ); + } + } +} + +/** + * Test table getters selection methods. + * + * @param aIdentifier [in] table accessible identifier + * @param aCellsArray [in] two dimensional array (row X columns) of cells + * states (either boolean (selected/unselected) if cell is + * origin, otherwise kRowSpanned or kColSpanned constant). + * @param aMsg [in] text appended before every message + */ +function testTableSelection(aIdentifier, aCellsArray, aMsg) { + var msg = aMsg ? aMsg : ""; + var acc = getAccessible(aIdentifier, [nsIAccessibleTable]); + if (!acc) { + return; + } + + var rowCount = aCellsArray.length; + var colsCount = aCellsArray[0].length; + + // Columns selection tests. + var selCols = []; + + // isColumnSelected test + for (let colIdx = 0; colIdx < colsCount; colIdx++) { + var isColSelected = true; + for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) { + if ( + !aCellsArray[rowIdx][colIdx] || + aCellsArray[rowIdx][colIdx] == undefined + ) { + isColSelected = false; + break; + } + } + + is( + acc.isColumnSelected(colIdx), + isColSelected, + msg + + "Wrong selection state of " + + colIdx + + " column for " + + prettyName(aIdentifier) + ); + + if (isColSelected) { + selCols.push(colIdx); + } + } + + // selectedColsCount test + is( + acc.selectedColumnCount, + selCols.length, + msg + "Wrong count of selected columns for " + prettyName(aIdentifier) + ); + + // getSelectedColumns test + var actualSelCols = acc.getSelectedColumnIndices(); + + var actualSelColsCount = actualSelCols.length; + is( + actualSelColsCount, + selCols.length, + msg + + "Wrong count of selected columns for " + + prettyName(aIdentifier) + + "from getSelectedColumns." + ); + + for (let i = 0; i < actualSelColsCount; i++) { + is( + actualSelCols[i], + selCols[i], + msg + "Column at index " + selCols[i] + " should be selected." + ); + } + + // Rows selection tests. + var selRows = []; + + // isRowSelected test + for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) { + var isRowSelected = true; + for (let colIdx = 0; colIdx < colsCount; colIdx++) { + if ( + !aCellsArray[rowIdx][colIdx] || + aCellsArray[rowIdx][colIdx] == undefined + ) { + isRowSelected = false; + break; + } + } + + is( + acc.isRowSelected(rowIdx), + isRowSelected, + msg + + "Wrong selection state of " + + rowIdx + + " row for " + + prettyName(aIdentifier) + ); + + if (isRowSelected) { + selRows.push(rowIdx); + } + } + + // selectedRowCount test + is( + acc.selectedRowCount, + selRows.length, + msg + "Wrong count of selected rows for " + prettyName(aIdentifier) + ); + + // getSelectedRows test + var actualSelRows = acc.getSelectedRowIndices(); + + var actualSelrowCount = actualSelRows.length; + is( + actualSelrowCount, + selRows.length, + msg + + "Wrong count of selected rows for " + + prettyName(aIdentifier) + + "from getSelectedRows." + ); + + for (let i = 0; i < actualSelrowCount; i++) { + is( + actualSelRows[i], + selRows[i], + msg + "Row at index " + selRows[i] + " should be selected." + ); + } + + // Cells selection tests. + var selCells = []; + + // isCellSelected test + for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) { + for (let colIdx = 0; colIdx < colsCount; colIdx++) { + if (aCellsArray[rowIdx][colIdx] & kSpanned) { + continue; + } + + var isSelected = !!aCellsArray[rowIdx][colIdx]; + is( + acc.isCellSelected(rowIdx, colIdx), + isSelected, + msg + + "Wrong selection state of cell at " + + rowIdx + + " row and " + + colIdx + + " column for " + + prettyName(aIdentifier) + ); + + if (aCellsArray[rowIdx][colIdx]) { + selCells.push(acc.getCellIndexAt(rowIdx, colIdx)); + } + } + } + + // selectedCellCount tests + is( + acc.selectedCellCount, + selCells.length, + msg + "Wrong count of selected cells for " + prettyName(aIdentifier) + ); + + // getSelectedCellIndices test + var actualSelCells = acc.getSelectedCellIndices(); + + var actualSelCellsCount = actualSelCells.length; + is( + actualSelCellsCount, + selCells.length, + msg + + "Wrong count of selected cells for " + + prettyName(aIdentifier) + + "from getSelectedCells." + ); + + for (let i = 0; i < actualSelCellsCount; i++) { + is( + actualSelCells[i], + selCells[i], + msg + + "getSelectedCellIndices: Cell at index " + + selCells[i] + + " should be selected." + ); + } + + // selectedCells and isSelected tests + var actualSelCellsArray = acc.selectedCells; + for (let i = 0; i < actualSelCellsCount; i++) { + var actualSelCellAccessible = actualSelCellsArray.queryElementAt( + i, + nsIAccessibleTableCell + ); + + let colIdx = acc.getColumnIndexAt(selCells[i]); + let rowIdx = acc.getRowIndexAt(selCells[i]); + var expectedSelCellAccessible = acc.getCellAt(rowIdx, colIdx); + + is( + actualSelCellAccessible, + expectedSelCellAccessible, + msg + + "getSelectedCells: Cell at index " + + selCells[i] + + " should be selected." + ); + + ok( + actualSelCellAccessible.isSelected(), + "isSelected: Cell at index " + selCells[i] + " should be selected." + ); + } + + // selected states tests + for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) { + for (let colIdx = 0; colIdx < colsCount; colIdx++) { + if (aCellsArray[rowIdx][colIdx] & kSpanned) { + continue; + } + + var cell = acc.getCellAt(rowIdx, colIdx); + var isSel = aCellsArray[rowIdx][colIdx]; + if (isSel == undefined) { + testStates(cell, 0, 0, STATE_SELECTABLE | STATE_SELECTED); + } else if (isSel) { + testStates(cell, STATE_SELECTED); + } else { + testStates(cell, STATE_SELECTABLE, 0, STATE_SELECTED); + } + } + } +} + +/** + * Test unselectColumn method of accessible table. + */ +function testUnselectTableColumn(aIdentifier, aColIdx, aCellsArray) { + var acc = getAccessible(aIdentifier, [nsIAccessibleTable]); + if (!acc) { + return; + } + + var rowCount = aCellsArray.length; + for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) { + // Unselect origin cell. + var [origRowIdx, origColIdx] = getOrigRowAndColumn( + aCellsArray, + rowIdx, + aColIdx + ); + aCellsArray[origRowIdx][origColIdx] = false; + } + + acc.unselectColumn(aColIdx); + testTableSelection( + aIdentifier, + aCellsArray, + "Unselect " + aColIdx + " column: " + ); +} + +/** + * Test selectColumn method of accessible table. + */ +function testSelectTableColumn(aIdentifier, aColIdx, aCellsArray) { + var acc = getAccessible(aIdentifier, [nsIAccessibleTable]); + if (!acc) { + return; + } + + var rowCount = aCellsArray.length; + var colsCount = aCellsArray[0].length; + + for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) { + for (var colIdx = 0; colIdx < colsCount; colIdx++) { + var cellState = aCellsArray[rowIdx][colIdx]; + + if (colIdx == aColIdx) { + // select target column + if (!(cellState & kSpanned)) { + // Select the cell if it is origin. + aCellsArray[rowIdx][colIdx] = true; + } else { + // If the cell is spanned then search origin cell and select it. + var [origRowIdx, origColIdx] = getOrigRowAndColumn( + aCellsArray, + rowIdx, + colIdx + ); + aCellsArray[origRowIdx][origColIdx] = true; + } + } else if (!(cellState & kSpanned)) { + // unselect other columns + if (colIdx > aColIdx) { + // Unselect the cell if traversed column index is greater than column + // index of target cell. + aCellsArray[rowIdx][colIdx] = false; + } else if (!(aCellsArray[rowIdx][aColIdx] & kColSpanned)) { + // Unselect the cell if the target cell is not row spanned. + aCellsArray[rowIdx][colIdx] = false; + } else { + // Unselect the cell if it is not spanned to the target cell. + for ( + var spannedColIdx = colIdx + 1; + spannedColIdx < aColIdx; + spannedColIdx++ + ) { + var spannedCellState = aCellsArray[rowIdx][spannedColIdx]; + if (!(spannedCellState & kRowSpanned)) { + aCellsArray[rowIdx][colIdx] = false; + break; + } + } + } + } + } + } + + acc.selectColumn(aColIdx); + testTableSelection( + aIdentifier, + aCellsArray, + "Select " + aColIdx + " column: " + ); +} + +/** + * Test unselectRow method of accessible table. + */ +function testUnselectTableRow(aIdentifier, aRowIdx, aCellsArray) { + var acc = getAccessible(aIdentifier, [nsIAccessibleTable]); + if (!acc) { + return; + } + + var colsCount = aCellsArray[0].length; + for (var colIdx = 0; colIdx < colsCount; colIdx++) { + // Unselect origin cell. + var [origRowIdx, origColIdx] = getOrigRowAndColumn( + aCellsArray, + aRowIdx, + colIdx + ); + aCellsArray[origRowIdx][origColIdx] = false; + } + + acc.unselectRow(aRowIdx); + testTableSelection( + aIdentifier, + aCellsArray, + "Unselect " + aRowIdx + " row: " + ); +} + +/** + * Test selectRow method of accessible table. + */ +function testSelectTableRow(aIdentifier, aRowIdx, aCellsArray) { + var acc = getAccessible(aIdentifier, [nsIAccessibleTable]); + if (!acc) { + return; + } + + var rowCount = aCellsArray.length; + var colsCount = aCellsArray[0].length; + + for (var rowIdx = 0; rowIdx < rowCount; rowIdx++) { + for (var colIdx = 0; colIdx < colsCount; colIdx++) { + var cellState = aCellsArray[rowIdx][colIdx]; + + if (rowIdx == aRowIdx) { + // select the given row + if (!(cellState & kSpanned)) { + // Select the cell if it is origin. + aCellsArray[rowIdx][colIdx] = true; + } else { + // If the cell is spanned then search origin cell and select it. + var [origRowIdx, origColIdx] = getOrigRowAndColumn( + aCellsArray, + rowIdx, + colIdx + ); + + aCellsArray[origRowIdx][origColIdx] = true; + } + } else if (!(cellState & kSpanned)) { + // unselect other rows + if (rowIdx > aRowIdx) { + // Unselect the cell if traversed row index is greater than row + // index of target cell. + aCellsArray[rowIdx][colIdx] = false; + } else if (!(aCellsArray[aRowIdx][colIdx] & kRowSpanned)) { + // Unselect the cell if the target cell is not row spanned. + aCellsArray[rowIdx][colIdx] = false; + } else { + // Unselect the cell if it is not spanned to the target cell. + for ( + var spannedRowIdx = rowIdx + 1; + spannedRowIdx < aRowIdx; + spannedRowIdx++ + ) { + var spannedCellState = aCellsArray[spannedRowIdx][colIdx]; + if (!(spannedCellState & kRowSpanned)) { + aCellsArray[rowIdx][colIdx] = false; + break; + } + } + } + } + } + } + + acc.selectRow(aRowIdx); + testTableSelection(aIdentifier, aCellsArray, "Select " + aRowIdx + " row: "); +} + +/** + * Test columnHeaderCells and rowHeaderCells of accessible table. + */ +function testHeaderCells(aHeaderInfoMap) { + for (var testIdx = 0; testIdx < aHeaderInfoMap.length; testIdx++) { + var dataCellIdentifier = aHeaderInfoMap[testIdx].cell; + var dataCell = getAccessible(dataCellIdentifier, [nsIAccessibleTableCell]); + + // row header cells + var rowHeaderCells = aHeaderInfoMap[testIdx].rowHeaderCells; + var rowHeaderCellsCount = rowHeaderCells.length; + var actualRowHeaderCells = dataCell.rowHeaderCells; + var actualRowHeaderCellsCount = actualRowHeaderCells.length; + + is( + actualRowHeaderCellsCount, + rowHeaderCellsCount, + "Wrong number of row header cells for the cell " + + prettyName(dataCellIdentifier) + ); + + if (actualRowHeaderCellsCount == rowHeaderCellsCount) { + for (let idx = 0; idx < rowHeaderCellsCount; idx++) { + var rowHeaderCell = getAccessible(rowHeaderCells[idx]); + var actualRowHeaderCell = actualRowHeaderCells.queryElementAt( + idx, + nsIAccessible + ); + isObject( + actualRowHeaderCell, + rowHeaderCell, + "Wrong row header cell at index " + + idx + + " for the cell " + + dataCellIdentifier + ); + } + } + + // column header cells + var colHeaderCells = aHeaderInfoMap[testIdx].columnHeaderCells; + var colHeaderCellsCount = colHeaderCells.length; + var actualColHeaderCells = dataCell.columnHeaderCells; + var actualColHeaderCellsCount = actualColHeaderCells.length; + + is( + actualColHeaderCellsCount, + colHeaderCellsCount, + "Wrong number of column header cells for the cell " + + prettyName(dataCellIdentifier) + ); + + if (actualColHeaderCellsCount == colHeaderCellsCount) { + for (let idx = 0; idx < colHeaderCellsCount; idx++) { + var colHeaderCell = getAccessible(colHeaderCells[idx]); + var actualColHeaderCell = actualColHeaderCells.queryElementAt( + idx, + nsIAccessible + ); + isObject( + actualColHeaderCell, + colHeaderCell, + "Wrong column header cell at index " + + idx + + " for the cell " + + dataCellIdentifier + ); + } + } + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// private implementation + +/** + * Return row and column of orig cell for the given spanned cell. + */ +function getOrigRowAndColumn(aCellsArray, aRowIdx, aColIdx) { + var cellState = aCellsArray[aRowIdx][aColIdx]; + + var origRowIdx = aRowIdx, + origColIdx = aColIdx; + if (cellState & kRowSpanned) { + for (var prevRowIdx = aRowIdx - 1; prevRowIdx >= 0; prevRowIdx--) { + let prevCellState = aCellsArray[prevRowIdx][aColIdx]; + if (!(prevCellState & kRowSpanned)) { + origRowIdx = prevRowIdx; + break; + } + } + } + + if (cellState & kColSpanned) { + for (var prevColIdx = aColIdx - 1; prevColIdx >= 0; prevColIdx--) { + let prevCellState = aCellsArray[aRowIdx][prevColIdx]; + if (!(prevCellState & kColSpanned)) { + origColIdx = prevColIdx; + break; + } + } + } + + return [origRowIdx, origColIdx]; +} diff --git a/accessible/tests/mochitest/table/a11y.ini b/accessible/tests/mochitest/table/a11y.ini new file mode 100644 index 0000000000..550fc54e04 --- /dev/null +++ b/accessible/tests/mochitest/table/a11y.ini @@ -0,0 +1,24 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_css_tables.html] +[test_headers_ariagrid.html] +[test_headers_ariatable.html] +[test_headers_table.html] +[test_headers_tree.xhtml] +[test_indexes_ariagrid.html] +[test_indexes_table.html] +[test_indexes_tree.xhtml] +[test_layoutguess.html] +[test_mtable.html] +[test_sels_ariagrid.html] +[test_sels_table.html] +[test_sels_tree.xhtml] +[test_struct_ariagrid.html] +[test_struct_ariatreegrid.html] +[test_struct_table.html] +[test_struct_tree.xhtml] +[test_table_1.html] +[test_table_2.html] +[test_table_mutation.html] diff --git a/accessible/tests/mochitest/table/test_css_tables.html b/accessible/tests/mochitest/table/test_css_tables.html new file mode 100644 index 0000000000..65877564e4 --- /dev/null +++ b/accessible/tests/mochitest/table/test_css_tables.html @@ -0,0 +1,114 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> + <title>CSS display:table is not a table</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // elements with display:table + + // only display:table + var accTree = + { SECTION: [ + { TEXT_LEAF: [ ] }, + ] }; + testAccessibleTree("table1", accTree); + + // only display:table and display:table-cell + accTree = + { SECTION: [ + { SECTION: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }; + testAccessibleTree("table2", accTree); + + // display:table, display:table-row, and display:table-cell + accTree = + { SECTION: [ + { SECTION: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }; + testAccessibleTree("table3", accTree); + + // display:table, display:table-row-group, display:table-row, and display:table-cell + accTree = + { SECTION: [ + { SECTION: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }; + testAccessibleTree("table4", accTree); + + // display:inline-table + accTree = + { TEXT_CONTAINER: [ + { TEXT_CONTAINER: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }; + testAccessibleTree("table5", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + title=" div with display:table exposes table semantics" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1007975">Mozilla Bug 1007975</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="table1" style="display:table"> + table1 + </div> + + <div id="table2" style="display:table"> + <div style="display:table-cell">table2</div> + </div> + + <div id="table3" style="display:table"> + <div style="display:table-row"> + <div style="display:table-cell">table3</div> + </div> + </div> + + <div id="table4" style="display:table"> + <div style="display:table-row-group"> + <div style="display:table-row"> + <div style="display:table-cell">table4</div> + </div> + </div> + </div> + + <div> + <span id="table5" style="display:inline-table"> + <span style="display:table-row"> + <span style="display:table-cell">table5</div> + </span> + </span> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_headers_ariagrid.html b/accessible/tests/mochitest/table/test_headers_ariagrid.html new file mode 100644 index 0000000000..7b2c3f3dbf --- /dev/null +++ b/accessible/tests/mochitest/table/test_headers_ariagrid.html @@ -0,0 +1,183 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> + <title>Table header information cells for ARIA grid</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // column and row headers from markup + + let headerInfoMap = [ + { + cell: "table_dc_1", + rowHeaderCells: [ "table_rh_1" ], + columnHeaderCells: [ "table_ch_2" ], + }, + { + cell: "table_dc_2", + rowHeaderCells: [ "table_rh_1" ], + columnHeaderCells: [ "table_ch_3" ], + }, + { + cell: "table_dc_3", + rowHeaderCells: [ "table_rh_2" ], + columnHeaderCells: [ "table_ch_2" ], + }, + { + cell: "table_dc_4", + rowHeaderCells: [ "table_rh_2" ], + columnHeaderCells: [ "table_ch_3" ], + }, + { + cell: "table_rh_1", + rowHeaderCells: [], + columnHeaderCells: [ "table_ch_1" ], + }, + { + cell: "table_rh_2", + rowHeaderCells: [], + columnHeaderCells: [ "table_ch_1" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + + // //////////////////////////////////////////////////////////////////////// + // column and row headers from markup for grid. + + headerInfoMap = [ + { + // not focusable cell (ARIAGridCellAccessible is used) + cell: "table2_dc_1", + rowHeaderCells: [], + columnHeaderCells: [ "table2_ch_1" ], + }, + { + // focusable cell (ARIAGridCellAccessible is used) + cell: "table2_dc_2", + rowHeaderCells: [], + columnHeaderCells: [ "table2_ch_2" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + + // //////////////////////////////////////////////////////////////////////// + // column and row headers from markup for one more grid. + + headerInfoMap = [ + { + // ARIAGridCellAccessible is used + cell: "t3_dc_1", + rowHeaderCells: [ "t3_rh_1" ], + columnHeaderCells: [ ], + }, + { + // ARIAGridCellAccessible is used (inside rowgroup) + cell: "t3_dc_2", + rowHeaderCells: [ "t3_rh_2" ], + columnHeaderCells: [ ], + }, + ]; + + testHeaderCells(headerInfoMap); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + title="implement IAccessibleTable2" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424">Mozilla Bug 512424</a> + <a target="_blank" + title="nsHTMLTableCellAccessible is used in dojo's ARIA grid" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=513848">Mozilla Bug 513848</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="grid"> + <div role="row"> + <span id="table_ch_1" role="columnheader">col_1</span> + <span id="table_ch_2" role="columnheader">col_2</span> + <span id="table_ch_3" role="columnheader">col_3</span> + </div> + <div role="row"> + <span id="table_rh_1" role="rowheader">row_1</span> + <span id="table_dc_1" role="gridcell">cell1</span> + <span id="table_dc_2" role="gridcell">cell2</span> + </div> + <div role="row"> + <span id="table_rh_2" role="rowheader">row_2</span> + <span id="table_dc_3" role="gridcell">cell3</span> + <span id="table_dc_4" role="gridcell">cell4</span> + </div> + </div> + + <div role="grid"> + <div role="row"> + <table role="presentation"> + <tr> + <td id="table2_ch_1" role="columnheader">header1</td> + <td id="table2_ch_2" role="columnheader">header2</td> + </tr> + </table> + </div> + <div role="row"> + <table role="presentation"> + <tr> + <td id="table2_dc_1" role="gridcell">cell1</td> + <td id="table2_dc_2" role="gridcell" tabindex="-1">cell2</td> + </tr> + </table> + </div> + </div> + + <div role="grid"> + <table role="presentation"> + <tbody role="presentation"> + <tr role="row"> + <th id="t3_rh_1" role="rowheader">Row 1</th> + <td id="t3_dc_1" role="gridcell" tabindex="-1"> + Apple Inc. + </td> + </tr> + </tbody> + </table> + <div role="rowgroup" tabindex="0"> + <table role="presentation"> + <tbody role="presentation"> + <tr role="row"> + <th id="t3_rh_2" role="rowheader">Row 2</th> + <td id="t3_dc_2" role="gridcell" tabindex="-1"> + Apple-Shmapple Inc. + </td> + </tr> + </tbody> + </table> + </div> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_headers_ariatable.html b/accessible/tests/mochitest/table/test_headers_ariatable.html new file mode 100644 index 0000000000..1af3813cdc --- /dev/null +++ b/accessible/tests/mochitest/table/test_headers_ariatable.html @@ -0,0 +1,94 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> + <title>Table header information cells for ARIA table</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // column and row headers from markup + + const headerInfoMap = [ + { + cell: "table_dc_1", + rowHeaderCells: [ "table_rh_1" ], + columnHeaderCells: [ "table_ch_2" ], + }, + { + cell: "table_dc_2", + rowHeaderCells: [ "table_rh_1" ], + columnHeaderCells: [ "table_ch_3" ], + }, + { + cell: "table_dc_3", + rowHeaderCells: [ "table_rh_2" ], + columnHeaderCells: [ "table_ch_2" ], + }, + { + cell: "table_dc_4", + rowHeaderCells: [ "table_rh_2" ], + columnHeaderCells: [ "table_ch_3" ], + }, + { + cell: "table_rh_1", + rowHeaderCells: [], + columnHeaderCells: [ "table_ch_1" ], + }, + { + cell: "table_rh_2", + rowHeaderCells: [], + columnHeaderCells: [ "table_ch_1" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + title="support ARIA table and cell roles" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173364">Bug 1173364</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="table"> + <div role="row"> + <span id="table_ch_1" role="columnheader">col_1</span> + <span id="table_ch_2" role="columnheader">col_2</span> + <span id="table_ch_3" role="columnheader">col_3</span> + </div> + <div role="row"> + <span id="table_rh_1" role="rowheader">row_1</span> + <span id="table_dc_1" role="cell">cell1</span> + <span id="table_dc_2" role="cell">cell2</span> + </div> + <div role="row"> + <span id="table_rh_2" role="rowheader">row_2</span> + <span id="table_dc_3" role="cell">cell3</span> + <span id="table_dc_4" role="cell">cell4</span> + </div> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_headers_table.html b/accessible/tests/mochitest/table/test_headers_table.html new file mode 100644 index 0000000000..0d7dafff4b --- /dev/null +++ b/accessible/tests/mochitest/table/test_headers_table.html @@ -0,0 +1,756 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> + <title>Table header information cells for HTML table</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // column header from thead and row header from @scope inside of tfoot + + var headerInfoMap = [ + { + cell: "table1_cell_1", + rowHeaderCells: [], + columnHeaderCells: [ "table1_weekday", "table1_date" ], + }, + { + cell: "table1_cell_2", + rowHeaderCells: [], + columnHeaderCells: [ "table1_day", "table1_date" ], + }, + { + cell: "table1_cell_3", + rowHeaderCells: [], + columnHeaderCells: [ "table1_qty" ], + }, + { + cell: "table1_cell_4", + rowHeaderCells: [], + columnHeaderCells: [ "table1_weekday", "table1_date" ], + }, + { + cell: "table1_cell_5", + rowHeaderCells: [], + columnHeaderCells: [ "table1_day", "table1_date" ], + }, + { + cell: "table1_cell_6", + rowHeaderCells: [], + columnHeaderCells: [ "table1_qty" ], + }, + { + cell: "table1_cell_7", + rowHeaderCells: [ "table1_total" ], + columnHeaderCells: [ "table1_qty" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // column and row headers from thead and @scope + + headerInfoMap = [ + { + cell: "table2_cell_2", + rowHeaderCells: [ "table2_rh_1" ], + columnHeaderCells: [ "table2_ch_2" ], + }, + { + cell: "table2_cell_3", + rowHeaderCells: [ "table2_rh_1" ], + columnHeaderCells: [ "table2_ch_3" ], + }, + { + cell: "table2_cell_5", + rowHeaderCells: [ "table2_rh_2" ], + columnHeaderCells: [ "table2_ch_2" ], + }, + { + cell: "table2_cell_6", + rowHeaderCells: [ "table2_rh_2" ], + columnHeaderCells: [ "table2_ch_3" ], + }, + { + cell: "table2_rh_1", + rowHeaderCells: [], + columnHeaderCells: [ "table2_ch_1" ], + }, + { + cell: "table2_rh_2", + rowHeaderCells: [], + columnHeaderCells: [ "table2_ch_1" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // column headers from @headers + + headerInfoMap = [ + { + cell: "table3_cell_1", + rowHeaderCells: [], + columnHeaderCells: [ "table3_ch_1" ], + }, + { + cell: "table3_cell_2", + rowHeaderCells: [], + columnHeaderCells: [ "table3_ch_2" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // table consisted of one column + + headerInfoMap = [ + { + cell: "table4_cell", + rowHeaderCells: [], + columnHeaderCells: [ "table4_ch" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // table consisted of one row + + headerInfoMap = [ + { + cell: "table5_cell", + rowHeaderCells: [ "table5_rh" ], + columnHeaderCells: [ ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // @headers points to table cells + + headerInfoMap = [ + { + cell: "table6_cell", + rowHeaderCells: [ "table6_rh" ], + columnHeaderCells: [ "table6_ch" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // @scope="rowgroup" and @scope="row" + + headerInfoMap = [ + { + cell: "t7_r1c1", + rowHeaderCells: [ "t7_Mary", "t7_Females" ], + columnHeaderCells: [ "t7_1km" ], + }, + { + cell: "t7_r1c2", + rowHeaderCells: [ "t7_Mary", "t7_Females" ], + columnHeaderCells: [ "t7_5km" ], + }, + { + cell: "t7_r1c3", + rowHeaderCells: [ "t7_Mary", "t7_Females" ], + columnHeaderCells: [ "t7_10km" ], + }, + { + cell: "t7_r2c1", + rowHeaderCells: [ "t7_Betsy", "t7_Females" ], + columnHeaderCells: [ "t7_1km" ], + }, + { + cell: "t7_r2c2", + rowHeaderCells: [ "t7_Betsy", "t7_Females" ], + columnHeaderCells: [ "t7_5km" ], + }, + { + cell: "t7_r2c3", + rowHeaderCells: [ "t7_Betsy", "t7_Females" ], + columnHeaderCells: [ "t7_10km" ], + }, + { + cell: "t7_r3c1", + rowHeaderCells: [ "t7_Matt", "t7_Males" ], + columnHeaderCells: [ "t7_1km" ], + }, + { + cell: "t7_r3c2", + rowHeaderCells: [ "t7_Matt", "t7_Males" ], + columnHeaderCells: [ "t7_5km" ], + }, + { + cell: "t7_r3c3", + rowHeaderCells: [ "t7_Matt", "t7_Males" ], + columnHeaderCells: [ "t7_10km" ], + }, + { + cell: "t7_r4c1", + rowHeaderCells: [ "t7_Todd", "t7_Males" ], + columnHeaderCells: [ "t7_1km" ], + }, + { + cell: "t7_r4c2", + rowHeaderCells: [ "t7_Todd", "t7_Males" ], + columnHeaderCells: [ "t7_5km" ], + }, + { + cell: "t7_r4c3", + rowHeaderCells: [ "t7_Todd", "t7_Males" ], + columnHeaderCells: [ "t7_10km" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // @scope="colgroup" and @scope="col" + + headerInfoMap = [ + { + cell: "t8_r1c1", + rowHeaderCells: [ "t8_1km" ], + columnHeaderCells: [ "t8_Mary", "t8_Females" ], + }, + { + cell: "t8_r1c2", + rowHeaderCells: [ "t8_1km" ], + columnHeaderCells: [ "t8_Betsy", "t8_Females" ], + }, + { + cell: "t8_r1c3", + rowHeaderCells: [ "t8_1km" ], + columnHeaderCells: [ "t8_Matt", "t8_Males" ], + }, + { + cell: "t8_r1c4", + rowHeaderCells: [ "t8_1km" ], + columnHeaderCells: [ "t8_Todd", "t8_Males" ], + }, + { + cell: "t8_r2c1", + rowHeaderCells: [ "t8_5km" ], + columnHeaderCells: [ "t8_Mary", "t8_Females" ], + }, + { + cell: "t8_r2c2", + rowHeaderCells: [ "t8_5km" ], + columnHeaderCells: [ "t8_Betsy", "t8_Females" ], + }, + { + cell: "t8_r2c3", + rowHeaderCells: [ "t8_5km" ], + columnHeaderCells: [ "t8_Matt", "t8_Males" ], + }, + { + cell: "t8_r2c4", + rowHeaderCells: [ "t8_5km" ], + columnHeaderCells: [ "t8_Todd", "t8_Males" ], + }, + { + cell: "t8_r3c1", + rowHeaderCells: [ "t8_10km" ], + columnHeaderCells: [ "t8_Mary", "t8_Females" ], + }, + { + cell: "t8_r3c2", + rowHeaderCells: [ "t8_10km" ], + columnHeaderCells: [ "t8_Betsy", "t8_Females" ], + }, + { + cell: "t8_r3c3", + rowHeaderCells: [ "t8_10km" ], + columnHeaderCells: [ "t8_Matt", "t8_Males" ], + }, + { + cell: "t8_r3c4", + rowHeaderCells: [ "t8_10km" ], + columnHeaderCells: [ "t8_Todd", "t8_Males" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // spanned table header cells (v1), @headers define header order + + headerInfoMap = [ + { + cell: "t9_r1c1", + rowHeaderCells: [ "t9_females", "t9_mary" ], + columnHeaderCells: [ "t9_1km" ], + }, + { + cell: "t9_r1c2", + rowHeaderCells: [ "t9_females", "t9_mary" ], + columnHeaderCells: [ "t9_5km" ], + }, + { + cell: "t9_r1c3", + rowHeaderCells: [ "t9_females", "t9_mary" ], + columnHeaderCells: [ "t9_10km" ], + }, + { + cell: "t9_r2c1", + rowHeaderCells: [ "t9_females", "t9_betsy" ], + columnHeaderCells: [ "t9_1km" ], + }, + { + cell: "t9_r2c2", + rowHeaderCells: [ "t9_females", "t9_betsy" ], + columnHeaderCells: [ "t9_5km" ], + }, + { + cell: "t9_r2c3", + rowHeaderCells: [ "t9_females", "t9_betsy" ], + columnHeaderCells: [ "t9_10km" ], + }, + { + cell: "t9_r3c1", + rowHeaderCells: [ "t9_males", "t9_matt" ], + columnHeaderCells: [ "t9_1km" ], + }, + { + cell: "t9_r3c2", + rowHeaderCells: [ "t9_males", "t9_matt" ], + columnHeaderCells: [ "t9_5km" ], + }, + { + cell: "t9_r3c3", + rowHeaderCells: [ "t9_males", "t9_matt" ], + columnHeaderCells: [ "t9_10km" ], + }, + { + cell: "t9_r4c1", + rowHeaderCells: [ "t9_males", "t9_todd" ], + columnHeaderCells: [ "t9_1km" ], + }, + { + cell: "t9_r4c2", + rowHeaderCells: [ "t9_males", "t9_todd" ], + columnHeaderCells: [ "t9_5km" ], + }, + { + cell: "t9_r4c3", + rowHeaderCells: [ "t9_males", "t9_todd" ], + columnHeaderCells: [ "t9_10km" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // spanned table header cells (v2), @headers define header order + + headerInfoMap = [ + { + cell: "t10_r1c1", + rowHeaderCells: [ "t10_1km" ], + columnHeaderCells: [ "t10_females", "t10_mary" ], + }, + { + cell: "t10_r1c2", + rowHeaderCells: [ "t10_1km" ], + columnHeaderCells: [ "t10_females", "t10_betsy" ], + }, + { + cell: "t10_r1c3", + rowHeaderCells: [ "t10_1km" ], + columnHeaderCells: [ "t10_males", "t10_matt" ], + }, + { + cell: "t10_r1c4", + rowHeaderCells: [ "t10_1km" ], + columnHeaderCells: [ "t10_males", "t10_todd" ], + }, + { + cell: "t10_r2c1", + rowHeaderCells: [ "t10_5km" ], + columnHeaderCells: [ "t10_females", "t10_mary" ], + }, + { + cell: "t10_r2c2", + rowHeaderCells: [ "t10_5km" ], + columnHeaderCells: [ "t10_females", "t10_betsy" ], + }, + { + cell: "t10_r2c3", + rowHeaderCells: [ "t10_5km" ], + columnHeaderCells: [ "t10_males", "t10_matt" ], + }, + { + cell: "t10_r2c4", + rowHeaderCells: [ "t10_5km" ], + columnHeaderCells: [ "t10_males", "t10_todd" ], + }, + { + cell: "t10_r3c1", + rowHeaderCells: [ "t10_10km" ], + columnHeaderCells: [ "t10_females", "t10_mary" ], + }, + { + cell: "t10_r3c2", + rowHeaderCells: [ "t10_10km" ], + columnHeaderCells: [ "t10_females", "t10_betsy" ], + }, + { + cell: "t10_r3c3", + rowHeaderCells: [ "t10_10km" ], + columnHeaderCells: [ "t10_males", "t10_matt" ], + }, + { + cell: "t10_r3c4", + rowHeaderCells: [ "t10_10km" ], + columnHeaderCells: [ "t10_males", "t10_todd" ], + }, + ]; + + testHeaderCells(headerInfoMap); + + // //////////////////////////////////////////////////////////////////////// + // Ensure correct column headers after colspan in a previous row. + headerInfoMap = [ + { + cell: "t11r1c1", + columnHeaderCells: [], + rowHeaderCells: [], + }, + { + cell: "t11r1c2", + columnHeaderCells: [], + rowHeaderCells: [], + }, + { + cell: "t11r2c1_2", + columnHeaderCells: ["t11r1c1"], + rowHeaderCells: [], + }, + { + cell: "t11r3c1", + columnHeaderCells: ["t11r1c1"], + rowHeaderCells: [], + }, + { + cell: "t11r3c2", + columnHeaderCells: ["t11r1c2"], + rowHeaderCells: [], + }, + ]; + testHeaderCells(headerInfoMap); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + title="implement IAccessibleTable2" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424"> + Bug 512424 + </a> + <a target="_blank" + title="Table headers not associated when header is a td element with no scope" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=704465"> + Bug 704465 + </a> + <a target="_blank" + title="Support rowgroup and colgroup HTML scope" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1141978"> + Bug 1141978 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table id="table1" border="1"> + <thead> + <tr> + <th id="table1_date" colspan="2">Date</th> + <th id="table1_qty" rowspan="2">Qty</th> + </tr> + <tr> + <th id="table1_weekday">Weekday</th> + <th id="table1_day">Day</th> + </tr> + </thead> + <tbody> + <tr> + <td id="table1_cell_1">Mon</td> + <td id="table1_cell_2">1</td> + <td id="table1_cell_3">20</td> + </tr> + <tr> + <td id="table1_cell_4">Thu</td> + <td id="table1_cell_5">2</td> + <td id="table1_cell_6">15</td> + </tr> + </tbody> + <tfoot> + <tr> + <th id="table1_total" scope="row" colspan="2">Total</th> + <td id="table1_cell_7">35</td> + </tr> + </tfoot> + </table> + + <table id="table2" border="1"> + <thead> + <tr> + <th id="table2_ch_1">col1</th> + <th id="table2_ch_2">col2</th> + <td id="table2_ch_3" scope="col">col3</td> + </tr> + </thead> + <tbody> + <tr> + <th id="table2_rh_1">row1</th> + <td id="table2_cell_2">cell1</td> + <td id="table2_cell_3">cell2</td> + </tr> + <tr> + <td id="table2_rh_2" scope="row">row2</td> + <td id="table2_cell_5">cell3</td> + <td id="table2_cell_6">cell4</td> + </tr> + </tbody> + </table> + + <table id="table3" border="1"> + <tr> + <td id="table3_cell_1" headers="table3_ch_1">cell1</td> + <td id="table3_cell_2" headers="table3_ch_2">cell2</td> + </tr> + <tr> + <td id="table3_ch_1" scope="col">col1</td> + <td id="table3_ch_2" scope="col">col2</td> + </tr> + </table> + + <table id="table4"> + <thead> + <tr> + <th id="table4_ch">colheader</th> + </tr> + </thead> + <tbody> + <tr> + <td id="table4_cell">bla</td> + </tr> + </tbody> + </table> + + <table id="table5"> + <tr> + <th id="table5_rh">rowheader</th> + <td id="table5_cell">cell</td> + </tr> + </table> + + <table id="table6"> + <tr> + <td>empty cell</th> + <td id="table6_ch">colheader</td> + </tr> + <tr> + <td id="table6_rh">rowheader</th> + <td id="table6_cell" headers="table6_ch table6_rh">cell</td> + </tr> + </table> + + <table id="table7" class="data complex" border="1"> + <caption>Version 1 with rowgroup</caption> + <thead> + <tr> + <td colspan="2"> </td> + <th id="t7_1km" scope="col">1 km</th> + <th id="t7_5km" scope="col">5 km</th> + <th id="t7_10km" scope="col">10 km</th> + </tr> + </thead> + <tbody> + <tr> + <th id="t7_Females" rowspan="2" scope="rowgroup">Females</th> + <th id="t7_Mary" scope="row">Mary</th> + <td id="t7_r1c1">8:32</td> + <td id="t7_r1c2">28:04</td> + <td id="t7_r1c3">1:01:16</td> + </tr> + <tr> + <th id="t7_Betsy" scope="row">Betsy</th> + <td id="t7_r2c1">7:43</td> + <td id="t7_r2c2">26:47</td> + <td id="t7_r2c3">55:38</td> + </tr> + <tr> + <th id="t7_Males" rowspan="2" scope="rowgroup">Males</th> + <th id="t7_Matt" scope="row">Matt</th> + <td id="t7_r3c1">7:55</td> + <td id="t7_r3c2">27:29</td> + <td id="t7_r3c3">57:04</td> + </tr> + <tr> + <th id="t7_Todd" scope="row">Todd</th> + <td id="t7_r4c1">7:01</td> + <td id="t7_r4c2">24:21</td> + <td id="t7_r4c3">50:35</td> + </tr> + </tbody> + </table> + + <table id="table8" class="data complex" border="1"> + <caption>Version 2 with colgroup</caption> + <thead> + <tr> + <td rowspan="2"> </td> + <th id="t8_Females" colspan="2" scope="colgroup">Females</th> + <th id="t8_Males" colspan="2" scope="colgroup">Males</th> + </tr> + <tr> + <th id="t8_Mary" scope="col">Mary</th> + <th id="t8_Betsy" scope="col">Betsy</th> + <th id="t8_Matt" scope="col">Matt</th> + <th id="t8_Todd" scope="col">Todd</th> + </tr> + </thead> + <tbody> + <tr> + <th id="t8_1km" scope="row">1 km</th> + <td id="t8_r1c1">8:32</td> + <td id="t8_r1c2">7:43</td> + <td id="t8_r1c3">7:55</td> + <td id="t8_r1c4">7:01</td> + </tr> + <tr> + <th id="t8_5km" scope="row">5 km</th> + <td id="t8_r2c1">28:04</td> + <td id="t8_r2c2">26:47</td> + <td id="t8_r2c3">27:27</td> + <td id="t8_r2c4">24:21</td> + </tr> + <tr> + <th id="t8_10km" scope="row">10 km</th> + <td id="t8_r3c1">1:01:16</td> + <td id="t8_r3c2">55:38</td> + <td id="t8_r3c3">57:04</td> + <td id="t8_r3c4">50:35</td> + </tr> + + </tbody> + </table> + + <table id="table9" border="1"> + <caption> + Example 1 (row group headers): + </caption> + <tr> + <td colspan="2"><span class="offscreen">empty</span></td> + <th id="t9_1km" width="40">1 km</th> + <th id="t9_5km" width="35">5 km</th> + <th id="t9_10km" width="42">10 km</th> + </tr> + <tr> + <th id="t9_females" width="56" rowspan="2">Females</th> + <th id="t9_mary" width="39">Mary</th> + <td id="t9_r1c1" headers="t9_females t9_mary t9_1km">8:32</td> + <td id="t9_r1c2" headers="t9_females t9_mary t9_5km">28:04</td> + <td id="t9_r1c3" headers="t9_females t9_mary t9_10km">1:01:16</td> + </tr> + <tr> + <th id="t9_betsy">Betsy</th> + <td id="t9_r2c1" headers="t9_females t9_betsy t9_1km">7:43</td> + <td id="t9_r2c2" headers="t9_females t9_betsy t9_5km">26:47</td> + <td id="t9_r2c3" headers="t9_females t9_betsy t9_10km">55:38</td> + </tr> + <tr> + <th id="t9_males" rowspan="2">Males</th> + <th id="t9_matt">Matt</th> + <td id="t9_r3c1" headers="t9_males t9_matt t9_1km">7:55</td> + <td id="t9_r3c2" headers="t9_males t9_matt t9_5km">27:29</td> + <td id="t9_r3c3" headers="t9_males t9_matt t9_10km">57:04</td> + </tr> + <tr> + <th id="t9_todd">Todd</th> + <td id="t9_r4c1" headers="t9_males t9_todd t9_1km">7:01</td> + <td id="t9_r4c2" headers="t9_males t9_todd t9_5km">24:21</td> + <td id="t9_r4c3" headers="t9_males t9_todd t9_10km">50:35</td> + </tr> + </table> + + <table id="table10" border="1"> + <caption> + Example 2 (column group headers): + </caption> + <tr> + <td rowspan="2"><span class="offscreen">empty</span></td> + <th colspan="2" id="t10_females">Females</th> + <th colspan="2" id="t10_males">Males</th> + </tr> + <tr> + <th width="40" id="t10_mary">Mary</th> + <th width="35" id="t10_betsy">Betsy</th> + <th width="42" id="t10_matt">Matt</th> + <th width="42" id="t10_todd">Todd</th> + </tr> + <tr> + <th width="39" id="t10_1km">1 km</th> + <td headers="t10_females t10_mary t10_1km" id="t10_r1c1">8:32</td> + <td headers="t10_females t10_betsy t10_1km" id="t10_r1c2">7:43</td> + <td headers="t10_males t10_matt t10_1km" id="t10_r1c3">7:55</td> + <td headers="t10_males t10_todd t10_1km" id="t10_r1c4">7:01</td> + </tr> + <tr> + <th id="t10_5km">5 km</th> + <td headers="t10_females t10_mary t10_5km" id="t10_r2c1">28:04</td> + <td headers="t10_females t10_betsy t10_5km" id="t10_r2c2">26:47</td> + <td headers="t10_males t10_matt t10_5km" id="t10_r2c3">27:29</td> + <td headers="t10_males t10_todd t10_5km" id="t10_r2c4">24:21</td> + </tr> + <tr> + <th id="t10_10km">10 km</th> + <td headers="t10_females t10_mary t10_10km" id="t10_r3c1">1:01:16</td> + <td headers="t10_females t10_betsy t10_10km" id="t10_r3c2">55:38</td> + <td headers="t10_males t10_matt t10_10km" id="t10_r3c3">57:04</td> + <td headers="t10_males t10_todd t10_10km" id="t10_r3c4">50:35</td> + </tr> + </table> + + <table id="table11"> + <tr> + <th id="t11r1c1">a</th> + <th id="t11r1c2">b</th> + </tr> + <tr> + <td id="t11r2c1_2" colspan="2"></td> + </tr> + <tr> + <td id="t11r3c1">e</td> + <td id="t11r3c2">f</td> + </tr> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_headers_tree.xhtml b/accessible/tests/mochitest/table/test_headers_tree.xhtml new file mode 100644 index 0000000000..46e8f43126 --- /dev/null +++ b/accessible/tests/mochitest/table/test_headers_tree.xhtml @@ -0,0 +1,100 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Table header information cells for XUL tree"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../table.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; + + function doTest() + { + var treeAcc = getAccessible("tree", [nsIAccessibleTable]); + + var headerInfoMap = [ + { + cell: treeAcc.getCellAt(0, 0), + rowHeaderCells: [], + columnHeaderCells: [ "col" ] + }, + { + cell: treeAcc.getCellAt(0, 1), + rowHeaderCells: [], + columnHeaderCells: [ "scol" ] + }, + { + cell: treeAcc.getCellAt(1, 0), + rowHeaderCells: [], + columnHeaderCells: [ "col" ] + }, + { + cell: treeAcc.getCellAt(1, 1), + rowHeaderCells: [], + columnHeaderCells: [ "scol" ] + }, + { + cell: treeAcc.getCellAt(2, 0), + rowHeaderCells: [], + columnHeaderCells: [ "col" ] + }, + { + cell: treeAcc.getCellAt(2, 1), + rowHeaderCells: [], + columnHeaderCells: [ "scol" ] + }, + ]; + + testHeaderCells(headerInfoMap); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(3)); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424" + title="implement IAccessibleTable2"> + Mozilla Bug 512424 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="debug"/> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + <treecol id="scol" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/table/test_indexes_ariagrid.html b/accessible/tests/mochitest/table/test_indexes_ariagrid.html new file mode 100644 index 0000000000..564e141a70 --- /dev/null +++ b/accessible/tests/mochitest/table/test_indexes_ariagrid.html @@ -0,0 +1,159 @@ +<!DOCTYPE html> +<html> +<head> + <title>Table indexes for ARIA grid tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // ARIA grid + var idxes = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11], + ]; + testTableIndexes("grid", idxes); + + idxes = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 8], + [9, 10, 11], + ]; + testTableIndexes("grid-rowgroups", idxes); + + // //////////////////////////////////////////////////////////////////////// + // a bit strange ARIA grid + idxes = [ + [0, 1], + [2, 3], + ]; + testTableIndexes("grid2", idxes); + + // an ARIA grid with div wrapping cell and div wrapping row + idxes = [ + [0, 1], + [2, 3], + [4, 5], + ]; + testTableIndexes("grid3", idxes); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=386813" + title="support nsIAccessibleTable on ARIA grid/treegrid">Mozilla Bug 386813</a> + <a target="_blank" + title="nsHTMLTableCellAccessible is used in dojo's ARIA grid" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=513848">Mozilla Bug 513848</a> + <a target="_blank" + title="ARIA grid with rowgroup breaks table row/col counting and indices" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=761853">Mozilla Bug 761853</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="grid" id="grid"> + <div role="row"> + <span role="columnheader">column1</span> + <span role="columnheader">column2</span> + <span role="columnheader">column3</span> + </div> + <div role="row"> + <span role="rowheader">row1</span> + <span role="gridcell">cell1</span> + <span role="gridcell">cell2</span> + </div> + <div role="row"> + <span role="rowheader">row2</span> + <span role="gridcell">cell3</span> + <span role="gridcell">cell4</span> + </div> + <div role="row"> + <span role="rowheader">row3</span> + <span role="gridcell">cell5</span> + <span role="gridcell">cell6</span> + </div> + </div> + + <div role="grid" id="grid-rowgroups"> + <div role="row"> + <span role="columnheader">grid-rowgroups-col1</span> + <span role="columnheader">grid-rowgroups-col2</span> + <span role="columnheader">grid-rowgroups-col3</span> + </div> + <div role="rowgroup"> + <div role="row"> + <span role="rowheader">grid-rowgroups-row1</span> + <span role="gridcell">grid-rowgroups-cell1</span> + <span role="gridcell">grid-rowgroups-cell2</span> + </div> + <div role="row"> + <span role="rowheader">grid-rowgroups-row2</span> + <span role="gridcell">grid-rowgroups-cell3</span> + <span role="gridcell">grid-rowgroups-cell4</span> + </div> + </div> + <div role="row"> + <span role="rowheader">grid-rowgroups-row3</span> + <span role="gridcell">grid-rowgroups-cell5</span> + <span role="gridcell">grid-rowgroups-cell6</span> + </div> + </div> + + <div role="grid" id="grid2"> + <div role="row"> + <table role="presentation"> + <tr> + <td role="columnheader">header1</td> + <td role="columnheader">header2</td> + </tr> + </table> + </div> + <div role="row"> + <table role="presentation"> + <tr> + <td role="gridcell">cell1</td> + <td role="gridcell" tabindex="-1">cell2</td> + </tr> + </table> + </div> + </div> + + <div role="grid" id="grid3"> + <div role="row"> + <div role="gridcell">Normal cell</div> + <div role="gridcell">1</div> + </div> + <div role="row"> + <div role="gridcell">Div</div> + <div><div role="gridcell">2</div></div> + </div> + <div tabindex="-1"><div role="row"> + <div role="gridcell">Cell in row in div</div> + <div role="gridcell">3</div> + </div></div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_indexes_table.html b/accessible/tests/mochitest/table/test_indexes_table.html new file mode 100644 index 0000000000..c43dbad8f7 --- /dev/null +++ b/accessible/tests/mochitest/table/test_indexes_table.html @@ -0,0 +1,481 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=410052 +--> +<head> + <title>Table indexes chrome tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // table + var idxes = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 7], + [6, 8, 9], + ]; + + testTableIndexes("table", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tableborder + idxes = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 7], + [6, 8, 9], + ]; + + testTableIndexes("tableborder", idxes); + + // //////////////////////////////////////////////////////////////////////// + // table + idxes = [ + [ 0, 1, 2, 2, 3, 4, 5, 6], + [ 7, 8, 9, 10, 11, 12, 13, 6], + [14, 15, 15, 16, 17, 18, 19, 6], + [20, 15, 15, 21, 22, 18, 23, 6], + ]; + + testTableIndexes("table2", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tableinsane1 (empty row groups) + idxes = [ + [0, 1, 2], + [3, 4, 5], + [6, 7, 7], + [6, 8, 9], + ]; + + testTableIndexes("tableinsane1", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tableinsane2 (empry rows) + idxes = [ + [-1, -1, -1], + [-1, -1, -1], + [ 0, 1, 2], + ]; + + testTableIndexes("tableinsane2", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tableinsane3 (cell holes) + idxes = [ + [0, 1, -1], + [2, 3, 4], + ]; + + testTableIndexes("tableinsane3", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tableinsane3.2 (cell holes, row spans, fixed in bug 417912) + idxes = [ + [0, 1, 2], + [3, -1, 2], + [4, 5, 2], + ]; + + testTableIndexes("tableinsane3.2", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tableinsane4 (empty row groups/rows and cell holes) + idxes = [ + [ 0, 1, 2], + [-1, -1, -1], + [ 3, 4, 5], + [ 6, 6, 7], + [ 8, -1, 7], + [ 9, 9, 9], + ]; + testTableIndexes("tableinsane4", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tableinsane5 (just a strange table) + idxes = [ + [ 0, 1, 2, -1, -1], + [-1, -1, -1, -1, -1], + [ 3, 4, 5, -1, -1], + [ 6, 7, -1, -1, -1], + [ 6, 8, 9, -1, -1], + [ 6, 10, 9, 11, 12], + ]; + testTableIndexes("tableinsane5", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tableinsane6 (overlapping cells, mad table) + idxes = [ + [ 0, 1, 2, -1, -1], + [-1, -1, -1, -1, -1], + [ 3, 4, 5, -1, -1], + [ 6, 6, 7, -1, -1], + [ 8, 9, 7, -1, -1], + [ 10, 9, 7, 11, 12], + ]; + testTableIndexes("tableinsane6", idxes); + + // //////////////////////////////////////////////////////////////////////// + // Table with a cell that has display: block; style + idxes = [ + [0, 1], + ]; + testTableIndexes("tablewithcelldisplayblock", idxes); + + // //////////////////////////////////////////////////////////////////////// + // A table with a cell that has display: block; and a cell with colspan. + // This makes us fall back to the ARIAGridCellAccessible implementation. + idxes = [ + [0, 0, 1], + ]; + testTableIndexes("tablewithcolspanandcelldisplayblock", idxes); + + // //////////////////////////////////////////////////////////////////////// + // A table with all elements being display:block, including a row group. + // This makes us fall back to the ARIAGridRowAccessible, and we must + // make sure the index is 0. Strange example from Gmail. + idxes = [ + [0], + ]; + testTableIndexes("tablealldisplayblock", idxes); + + // //////////////////////////////////////////////////////////////////////// + // Table that has display: block; style + idxes = [ + [0, 1], + ]; + testTableIndexes("tablewithdisplayblock", idxes); + + // //////////////////////////////////////////////////////////////////////// + // tbody that has display: block; style + idxes = [ + [0, 1], + ]; + testTableIndexes("tbodywithdisplayblock", idxes); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="GetIndexAt and GetRowAtIndex and GetColumnAtIndex on HTML tables are inconsistent" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052"> + Bug 410052 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- + If you change the structure of the table please make sure to change + the indexes count in 'for' statement in the script above. + --> + <table border="1" id="table"> + <caption><strong><b><font size="29">this is a caption for this table</font></b></strong></caption> + <thead> + <tr> + <th>col1</th> + <th>col2</th> + <th>col3</th> + </tr> + </thead> + <tbody> + <tr> + <td>1</td> + <td>2</td> + <td>3</td> + </tr> + <tr> + <td rowspan="0">4</td> + <td colspan="2">5</td> + </tr> + <tr> + <td>6</td> + <td>7</td> + </tr> + </tbody> + </table> + + <table border="1" id="tableborder" style="border-collapse:collapse"> + <caption><strong><b><font size="29">this is a caption for this bc table</font></b></strong></caption> + <thead> + <tr> + <th>col1</th> + <th>col2</th> + <th>col3</th> + </tr> + </thead> + <tbody> + <tr> + <td>1</td> + <td>2</td> + <td>3</td> + </tr> + <tr> + <td rowspan="2">4</td> + <td colspan="2">5</td> + </tr> + <tr> + <td>6</td> + <td>7</td> + </tr> + </tbody> + </table> + + <table cellpadding="2" cellspacing="2" border="1" width="50%" id="table2"> + <caption>column and row spans</caption> + <tbody> + <tr> + <td>0</td> + <td>1</td> + <td rowspan="1" colspan="2">2</td> + <td>3</td> + <td>4</td> + <td>5</td> + <td rowspan="4" colspan="1">6</td> + </tr> + <tr> + <td>7</td> + <td>8</td> + <td>8</td> + <td>10</td> + <td>11</td> + <td>12</td> + <td>13</td> + </tr> + <tr> + <td>14</td> + <td rowspan="2" colspan="2">15</td> + <td>16</td> + <td>17</td> + <td rowspan="2" colspan="1">18</td> + <td>19</td> + </tr> + <tr> + <td>20</td> + <td>21</td> + <td>22</td> + <td>23</td> + </tr> + </tbody> + </table> + + <table border="1" id="tableinsane1"> + <caption>test empty row groups</caption> + <thead> + <tr> + <th>col1</th> + <th>col2</th> + <th>col3</th> + </tr> + </thead> + <tbody></tbody> + <tbody></tbody> + <tbody></tbody> + <tbody> + <tr> + <td>1</td> + <td>2</td> + <td>3</td> + </tr> + <tr> + <td rowspan="2">4</td> + <td colspan="2">5</td> + </tr> + <tr> + <td>6</td> + <td>7</td> + </tr> + </tbody> + </table> + + <table border="1" id="tableinsane2"> + <caption>empty rows</caption> + <tbody><tr></tr><tr></tr></tbody> + <tbody></tbody> + <tbody> + <tr> + <td>0</td> + <td>1</td> + <td>2</td> + </tr> + </tbody> + </table> + + <table border="1" id="tableinsane3"> + <caption>missed cell</caption> + <tbody> + <tr> + <td>0</td> + <td>1</td> + </tr> + </tbody> + <tbody> + <tr> + <td>2</td> + <td>3</td> + <td>4</td> + </tr> + </tbody> + </table> + + <table cellpadding="2" cellspacing="2" border="1" id="tableinsane3.2"> + <tr><td>1</td><td>2</td><td rowspan=3>3</td> + <tr><td>4</td> + <tr><td>5</td><td>6</td> + </table> + + <table border="1" id="tableinsane4"> + <caption>test empty rows + cellmap holes</caption> + <thead> + <tr> + <th>col1</th> + <th>col2</th> + <th>col3</th> + </tr> + </thead> + <tbody><tr></tr></tbody> + <tbody></tbody> + <tbody></tbody> + <tbody> + <tr> + <td>1</td> + <td>2</td> + <td>3</td> + </tr> + <tr> + <td colspan="2">4</td> + <td rowspan="2">5</td> + </tr> + <tr> + <td>6</td> + </tr> + <tr> + <td colspan="3">7</td> + </tr> + + </tbody> + </table> + + <table border="1" id="tableinsane5"> + <caption>just a strange table</caption> + <thead> + <tr> + <th>col1</th> + <th>col2</th> + <th>col3</th> + </tr> + </thead> + <tbody><tr></tr></tbody> + <tbody></tbody> + <tbody></tbody> + <tbody> + <tr> + <td>1</td> + <td>2</td> + <td>3</td> + </tr> + <tr> + <td rowspan="0">4</td> + <td colspan="0">5</td> + </tr> + <tr> + <td>6</td> + <td rowspan="0">7</td> + </tr> + <tr> + <td>8</td> + <td>9</td> + <td>10</td> + </tr> + + </tbody> + </table> + + <table border="1" id="tableinsane6" > + <caption>overlapping cells</caption> + <thead> + <tr> + <th>header cell 0</th> + <th>header cell 1</th> + <th>header cell 2</th> + </tr> + </thead> + <tbody><tr></tr></tbody> + <tbody></tbody> + <tbody></tbody> + <tbody> + <tr> + <td>3</td> + <td>4</td> + <td>5</td> + </tr> + <tr> + <td colspan="2">6</td> + <td rowspan="0">7</td> + </tr> + <tr> + <td>8</td> + <td rowspan="0">9</td> + </tr> + <tr> + <td colspan="3">10</td> + <td>11</td> + <td>12</td> + </tr> + </tbody> + </table> + + <table id="tablewithcelldisplayblock"> + <tr> + <th>a</th> + <td style="display: block;">b</td> + </tr> + </table> + + <table id="tablewithcolspanandcelldisplayblock"> + <tr> + <th colspan="2">a</th> + <td style="display: block;" >b</td> + </tr> + </table> + + <table id="tablealldisplayblock" style="display:block;"> + <tbody style="display:block;"> + <tr style="display:block;"> + <td style="display:block;">text</td> + </tr> + </tbody> + </table> + + <table id="tablewithdisplayblock" style="display: block;"> + <tr><th>a</th><td>b</td></tr> + </table> + + <table id="tbodywithdisplayblock"> + <tbody style="display: block;"> + <tr> + <th>a</th> + <td>b</td> + </tr> + </tbody> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_indexes_tree.xhtml b/accessible/tests/mochitest/table/test_indexes_tree.xhtml new file mode 100644 index 0000000000..0b1b6b2625 --- /dev/null +++ b/accessible/tests/mochitest/table/test_indexes_tree.xhtml @@ -0,0 +1,70 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible Table indexes tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../table.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; + + function doTest() + { + var idxes = [ + [0, 1], + [2, 3], + [4, 5] + ]; + testTableIndexes("tree", idxes); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(3)); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Mozilla Bug 503727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="debug"/> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + <treecol id="scol" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/table/test_layoutguess.html b/accessible/tests/mochitest/table/test_layoutguess.html new file mode 100644 index 0000000000..f3e94c5a78 --- /dev/null +++ b/accessible/tests/mochitest/table/test_layoutguess.html @@ -0,0 +1,554 @@ +<html> +<!-- https://bugzilla.mozilla.org/show_bug.cgi?id=495388 --> +<head> + <title>test HTMLTableAccessible::IsProbablyForLayout implementation</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + function isLayoutTable(id) { + // This helps us know if the absence of layout-guess is simply because + // it is not a table. + ok(isAccessible(id, nsIAccessibleTable), `${id} has table interface`); + testAttrs(id, { "layout-guess": "true" }, true); + } + function isDataTable(id) { + testAbsentAttrs(id, { "layout-guess": "true" }); + } + + function doTest() { + // table with role of grid + isDataTable("table1"); + // table with role of grid and datatable="0" + isDataTable("table1.1"); + + // table with landmark role + isDataTable("table2"); + + // table with summary + isDataTable("table3"); + + // table with caption + isDataTable("table4"); + + // layout table with empty caption + isLayoutTable("table4.2"); + + // table with thead element + isDataTable("table5"); + + // table with tfoot element + isDataTable("table5.1"); + + // table with colgroup or col elements + isDataTable("table5.2"); + isDataTable("table5.3"); + + // table with th element + isDataTable("table6"); + + // table with headers attribute + isDataTable("table6.2"); + + // table with scope attribute + isDataTable("table6.2.2"); + + // table with abbr attribute + isDataTable("table6.2.3"); + + // table with abbr element + isDataTable("table6.3"); + + // table with abbr element having empty text node + isDataTable("table6.4"); + + // table with abbr element and non-empty text node + isLayoutTable("table6.5"); + + // layout table with nested table + isLayoutTable("table9"); + + // layout table with 1 column + isLayoutTable("table10"); + + // layout table with 1 row + isLayoutTable("table11"); + + // table with 5 columns + isDataTable("table12"); + + // table with a bordered cell + isDataTable("table13"); + + // table with alternating row background colors + isDataTable("table14"); + + // table with 3 columns and 21 rows + isDataTable("table15"); + + // layout table that has a 100% width + isLayoutTable("table16"); + + // layout table that has a 95% width in pixels + isLayoutTable("table17"); + + // layout table with less than 10 columns + isLayoutTable("table18"); + + // layout table with embedded iframe + isLayoutTable("table19"); + + // tree grid, no layout table + isDataTable("table20"); + + // layout table containing nested data table (having data structures) + isLayoutTable("table21"); + isLayoutTable("table21.2"); + isLayoutTable("table21.3"); + isLayoutTable("table21.4"); + isLayoutTable("table21.5"); + isLayoutTable("table21.6"); + + // layout table having datatable="0" attribute and containing data table structure (tfoot element) + isLayoutTable("table22"); + + // repurposed table for tabbed UI + isLayoutTable("table23"); + + // layout display:block table with 1 column + isLayoutTable("displayblock_table1"); + + // matrix + isDataTable("mtable1"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=495388" + title="Don't treat tables that have a landmark role as layout table"> + Mozilla Bug 495388 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=690222" + title="Data table elements used to determine layout-guess attribute shouldn't be picked from nested tables"> + Mozilla Bug 690222 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=696975" + title="Extend the list of legitimate data table structures"> + Mozilla Bug 696975 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Table with role of grid --> + <table id="table1" role="grid"> + <tr> + <th>Sender</th> + <th>Subject</th> + <th>Date</th> + </tr> + <tr> + <td>Marco</td> + <td>Test</td> + <td>June 12</td> + </tr> + <tr> + <td>David</td> + <td>Another test</td> + <td>June 12</td> + </tr> + <tr> + <td>Alex</td> + <td>Third test</td> + <td>June 12</td> + </tr> + </table> + <!-- table with role of grid and datatable="0"--> + <table id="table1.1" role="grid" datatable="0"> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </table> + + <!-- table with landmark role --> + <table id="table2" role="main"> + <tr> + <th>Sender</th> + <th>Subject</th> + <th>Date</th> + </tr> + <tr> + <td>Marco</td> + <td>Test</td> + <td>June 12</td> + </tr> + <tr> + <td>David</td> + <td>Another test</td> + <td>June 12</td> + </tr> + <tr> + <td>Alex</td> + <td>Third test</td> + <td>June 12</td> + </tr> + </table> + + <!-- table with summary --> + <table id="table3" summary="This is a table"> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </table> + + <!-- table with caption --> + <table id="table4"> + <caption>This is a table</caption> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </table> + + <!-- layout table with empty caption --> + <table id="table4.2"> + <caption> </caption> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </table> + + <!-- table with thead element --> + <table id="table5"> + <thead> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </thead> + </table> + + <!-- table with tfoot element --> + <table id="table5.1"> + <tfoot> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </tfoot> + </table> + + <!-- table with colgroup and col elements --> + <table id="table5.2"> + <colgroup width="20"></colgroup> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </table> + <table id="table5.3"> + <col width="20"> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </table> + + <!-- table with th element --> + <table id="table6"> + <tr> + <th>Cell1</th><th>cell2</th> + </tr> + </table> + + <!-- table with headers attribute --> + <table id="table6.2"> + <tr> + <td headers="a">table6.2 cell</td> + </tr> + </table> + + <!-- table with scope attribute --> + <table id="table6.2.2"> + <tr> + <td scope="a">table6.2.2 cell</td> + </tr> + </table> + + <!-- table with abbr attribute --> + <table id="table6.2.3"> + <tr> + <td abbr="table6.2.3">table6.2.3 cell1</td> + </tr> + </table> + + <!-- table with abbr element --> + <table id="table6.3"> + <tr> + <td>table6.3 cell1</td> + <td><abbr>table6.3 cell2</abbr></td> + </tr> + </table> + + <!-- table with abbr element having empty text node --> + <table id="table6.4"> + <tr> + <td> + <abbr>abbr</abbr> + </td> + </tr> + </table> + + <!-- table with abbr element and non-empty text node --> + <table id="table6.5"> + <tr> + <td> + This is a really long text (<abbr>tiarlt</abbr>) inside layout table + </td> + </tr> + </table> + + <!-- layout table with nested table --> + <table id="table9"> + <tr> + <td><table><tr><td>Cell</td></tr></table></td> + </tr> + </table> + + <!-- layout table with 1 column --> + <table id="table10"> + <tr><td>Row1</td></tr> + <tr><td>Row2</td></tr> + </table> + + <!-- layout table with 1 row and purposely many columns --> + <table id="table11"> + <tr><td>Col1</td><td>Col2</td><td>Col3</td><td>Col4</td><td>Col5</td></tr> + </table> + + <!-- table with 5 columns --> + <table id="table12"> + <tr><td>Col1</td><td>Col2</td><td>Col3</td><td>Col4</td><td>Col5</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td><td>Col4</td><td>Col5</td></tr> + </table> + + <!-- table with a bordered cell --> + <table id="table13" border="1" width="100%" bordercolor="#0000FF"> + <tr> + <td bordercolor="#000000"> </td> + <td bordercolor="#000000"> </td> + <td bordercolor="#000000"> </td> + </tr> + <tr> + <td bordercolor="#000000"> </td> + <td bordercolor="#000000"> </td> + <td bordercolor="#000000"> </td> + </tr> + </table> + + <!-- table with alternating row background colors --> + <table id="table14" width="100%"> + <tr style="background-color: #0000FF;"> + <td> </td> + <td> </td> + <td> </td> + </tr> + <tr style="background-color: #00FF00;"> + <td> </td> + <td> </td> + <td> </td> + </tr> + </table> + + <!-- table with 3 columns and 21 rows --> + <table id="table15" border="0"> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + </table> + + <!-- layout table that has a 100% width --> + <table id="table16" width="100%"> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + </table> + + <!-- layout table that has a 95% width in pixels --> + <table id="table17" width="98%"> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + <tr><td>Col1</td><td>Col2</td><td>Col3</td></tr> + </table> + + <!-- layout table with less than 10 columns --> + <table id="table18"> + <tr> + <td>Marco</td> + <td>Test</td> + <td>June 12</td> + </tr> + <tr> + <td>David</td> + <td>Another test</td> + <td>June 12</td> + </tr> + <tr> + <td>Alex</td> + <td>Third test</td> + <td>June 12</td> + </tr> + </table> + + <!-- layout table with embedded iframe --> + <table id="table19"> + <tr><td><iframe id="frame"></iframe></td><td> </td><td> </td></tr> + <tr><td> </td><td> </td><td> </td></tr> + <tr><td> </td><td> </td><td> </td></tr> + <tr><td> </td><td> </td><td> </td></tr> + </table> + + <!-- tree grid, no layout table --> + <table id="table20" role="treegrid"> + <tr role="treeitem"><td>Cell1</td><td>Cell2</td></tr> + </table> + + <!-- layout table with nested data table containing data table elements --> + <table id="table21"> + <tr> + <td> + <table> + <caption>table</caption> + <tr><td>Cell</td></tr> + </table> + </td> + </tr> + </table> + <table id="table21.2"> + <tr> + <td> + <table> + <colgroup width="20"></colgroup> + <tr><th>Cell</th></tr> + </table> + </td> + </tr> + </table> + <table id="table21.3"> + <tr> + <td> + <table> + <col width="20"></col> + <tr><th>Cell</th></tr> + </table> + </td> + </tr> + </table> + <table id="table21.4"> + <tr> + <td> + <table> + <tr><th>Cell</th></tr> + </table> + </td> + </tr> + </table> + <table id="table21.5"> + <tr> + <td> + <table> + <thead> + <tr><td>Cell</td></tr> + </thead> + </table> + </td> + </tr> + </table> + <table id="table21.6"> + <tr> + <td> + <table> + <tfoot> + <tr><td>Cell</td></tr> + </tfoot> + </table> + </td> + </tr> + </table> + + <!-- layout table with datatable="0" and tfoot element--> + <table id="table22" datatable="0"> + <tfoot> + <tr> + <td>Cell1</td><td>cell2</td> + </tr> + </tfoot> + </table> + + <table id="table23" border="1"> + <tr role="tablist"> + <td role="tab">Tab 1</td><td role="tab">Tab 2</td> + </tr> + <tr> + <td role="tabpanel" colspan="2">Hello</td> + </tr> + </table> + + <!-- display:block table --> + <table id="displayblock_table1" style="display:block"> + <tr><td>Row1</td></tr> + <tr><td>Row2</td></tr> + </table> + + <!-- MathML matrix --> + <math> + <mtable id="mtable1"> + <mtr> + <mtd> + <mn>1</mn> + </mtd> + <mtd> + <mn>0</mn> + </mtd> + </mtr> + <mtr> + <mtd> + <mn>0</mn> + </mtd> + <mtd> + <mn>1</mn> + </mtd> + </mtr> + </mtable> + </math> +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_mtable.html b/accessible/tests/mochitest/table/test_mtable.html new file mode 100644 index 0000000000..3e4d7ccc53 --- /dev/null +++ b/accessible/tests/mochitest/table/test_mtable.html @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<html> +<head> + <title>MathML table tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + function doTest() { + // 'Simple' table + var idxes = [ + [0, 1], + [2, 3], + ]; + testTableIndexes("simple", idxes); + var cellsArray = [ + [kDataCell, kDataCell], + [kDataCell, kDataCell], + ]; + var rowsArray = [ROLE_MATHML_TABLE_ROW, ROLE_MATHML_TABLE_ROW]; + testTableStruct("simple", cellsArray, kNoColumnHeader, + "", "", kMathTable, rowsArray); + + // 'Complex' table + idxes = [ + [0, 0, 0], + [1, 1, 2], + [1, 1, 3], + ]; + testTableIndexes("complex", idxes); + cellsArray = [ + [kDataCell, kColSpanned, kColSpanned], + [kDataCell, kColSpanned, kDataCell], + [kRowSpanned, kSpanned, kDataCell], + ]; + rowsArray = [ + ROLE_MATHML_TABLE_ROW, + ROLE_MATHML_TABLE_ROW, + ROLE_MATHML_TABLE_ROW, + ]; + testTableStruct("complex", cellsArray, kNoColumnHeader, + "", "", kMathTable, rowsArray); + + // 'Simple' table with mlabeledtr + // At the moment we do not implement mlabeledtr but just hide the label + // with display: none. Thus we just test the role for now. See bug 689641. + idxes = [[0]]; + testTableIndexes("simple_label", idxes); + cellsArray = [[kDataCell]]; + rowsArray = [ROLE_MATHML_LABELED_ROW]; + testTableStruct("simple_label", cellsArray, kNoColumnHeader, + "", "", kMathTable, rowsArray); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <math> + <mtable id="simple"> + <mtr> + <mtd> + <mn>1</mn> + </mtd> + <mtd> + <mn>0</mn> + </mtd> + </mtr> + <mtr> + <mtd> + <mn>0</mn> + </mtd> + <mtd> + <mn>1</mn> + </mtd> + </mtr> + </mtable> + + <mtable id="complex"> + <mtr> + <mtd columnspan="3"> + <mtext>1 x 3</mtext> + </mtd> + </mtr> + <mtr> + <mtd rowspan="2" columnspan="2"> + <mtext>2 x 2</mtext> + </mtd> + <mtd> + <mtext>1 x 1</mtext> + </mtd> + </mtr> + <mtr> + <mtd> + <mtext>1 x 1</mtext> + </mtd> + </mtr> + </mtable> + + <mtable id="simple_label"> + <mlabeledtr> + <mtd><mtext>1</mtext></mtd> + <mtd><mtext>label</mtext></mtd> + </mlabeledtr> + </mtable> + </math> + +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_sels_ariagrid.html b/accessible/tests/mochitest/table/test_sels_ariagrid.html new file mode 100644 index 0000000000..784939bf03 --- /dev/null +++ b/accessible/tests/mochitest/table/test_sels_ariagrid.html @@ -0,0 +1,159 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=410052 +--> +<head> + <title>nsIAccesible selection methods testing for ARIA grid</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // ARIA grid + var cellsArray = + [ + [ true, true, false, true], + [ true, false, true, true], + [ true, false, false, true], + [ true, true, true, true], + [ true, true, true, true], + ]; + + testTableSelection("table", cellsArray); + testUnselectTableColumn("table", 3, cellsArray); + testUnselectTableRow("table", 3, cellsArray); + testSelectTableColumn("table", 0, cellsArray); + testSelectTableRow("table", 0, cellsArray); + + // //////////////////////////////////////////////////////////////////////// + // a bit strange ARIA grid + cellsArray = + [ + [ false, false], + [ false, false], + ]; + + testTableSelection("grid2", cellsArray); + testSelectTableColumn("grid2", 0, cellsArray); + testSelectTableRow("grid2", 0, cellsArray); + testUnselectTableColumn("grid2", 0, cellsArray); + testUnselectTableRow("grid2", 0, cellsArray); + + // //////////////////////////////////////////////////////////////////////// + // ARIA grid (column and row headers) + + cellsArray = + [ + [ undefined, true, false], + [ undefined, true, false], + ]; + + testTableSelection("grid3", cellsArray); + testSelectTableColumn("grid3", 0, cellsArray); + testSelectTableRow("grid3", 0, cellsArray); + testUnselectTableColumn("grid3", 0, cellsArray); + testUnselectTableRow("grid3", 0, cellsArray); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="implement nsIAccessibleTable selection methods for ARIA grids" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052">Bug 410052</a> + <a target="_blank" + title="nsHTMLTableCellAccessible is used in dojo's ARIA grid" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=513848">Bug 513848</a> + <a target="_blank" + title="ARIA columnheader/rowheader shouldn't be selectable by default" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=888247">Bug 888247</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div role="grid" id="table"> + <div role="row"> + <span role="gridcell" aria-selected="true">cell1</span> + <span role="gridcell" aria-selected="true">cell2</span> + <span role="gridcell">cell3</span> + <span role="gridcell" aria-selected="true">cell4</span> + </div> + <div role="row"> + <span role="gridcell" aria-selected="true">cell5</span> + <span role="gridcell">cell6</span> + <span role="gridcell" aria-selected="true">cell7</span> + <span role="gridcell" aria-selected="true">cell8</span> + </div> + <div role="row"> + <span role="gridcell" aria-selected="true">cell9</span> + <span role="gridcell">cell10</span> + <span role="gridcell">cell11</span> + <span role="gridcell" aria-selected="true">cell12</span> + </div> + <div role="row" aria-selected="true"> + <span role="gridcell">cell13</span> + <span role="gridcell">cell14</span> + <span role="gridcell">cell15</span> + <span role="gridcell">cell16</span> + </div> + <div role="row"> + <span role="gridcell" aria-selected="true">cell17</span> + <span role="gridcell" aria-selected="true">cell18</span> + <span role="gridcell" aria-selected="true">cell19</span> + <span role="gridcell" aria-selected="true">cell20</span> + </div> + </div> + + <div role="grid" id="grid2"> + <div role="row"> + <table role="presentation"> + <tr> + <td role="columnheader" aria-selected="false">header1</td> + <td role="columnheader" aria-selected="false">header2</td> + </tr> + </table> + </div> + <div role="row"> + <table role="presentation"> + <tr> + <td role="gridcell">cell1</td> + <td role="gridcell" tabindex="-1">cell2</td> + </tr> + </table> + </div> + </div> + + <div role="grid" id="grid3"> + <div role="row"> + <div role="columnheader" id="colheader_default">col header1</div> + <div role="columnheader" id="colheader_selected" aria-selected="true">col header2</div> + <div role="columnheader" id="colheader_notselected" aria-selected="false">col header3</div> + </div> + <div role="row"> + <div role="rowheader" id="rowheader_default">row header1</div> + <div role="rowheader" id="rowheader_selected" aria-selected="true">row header2</div> + <div role="rowheader" id="rowheader_notselected" aria-selected="false">row header3</div> + </div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_sels_table.html b/accessible/tests/mochitest/table/test_sels_table.html new file mode 100644 index 0000000000..0c87112d17 --- /dev/null +++ b/accessible/tests/mochitest/table/test_sels_table.html @@ -0,0 +1,178 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <title>nsIAccesible selection methods testing for HTML table</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="text/javascript"> + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // table + + var cellsArray = + [ + [false, false, false, kColSpanned, false, false, false, false], + [false, false, false, false, false, false, false, kRowSpanned], + [false, false, kColSpanned, false, false, false, false, kRowSpanned], + [false, kRowSpanned, kSpanned, false, false, kRowSpanned, false, kRowSpanned], + ]; + + testTableSelection("table", cellsArray); + + var rowCount = 4; + for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) + testSelectTableRow("table", rowIdx, cellsArray); + + for (let rowIdx = 0; rowIdx < rowCount; rowIdx++) { + testSelectTableRow("table", rowIdx, cellsArray); + testUnselectTableRow("table", rowIdx, cellsArray); + } + + var columsCount = 8; + for (let colIdx = 0; colIdx < columsCount; colIdx++) + testSelectTableColumn("table", colIdx, cellsArray); + + for (let colIdx = 0; colIdx < columsCount; colIdx++) { + testSelectTableColumn("table", colIdx, cellsArray); + testUnselectTableColumn("table", colIdx, cellsArray); + } + + var accTable = getAccessible("table", [nsIAccessibleTable]); + ok(!accTable.isProbablyForLayout(), "table is not for layout"); + + // //////////////////////////////////////////////////////////////////////// + // table instane + + cellsArray = + [ + [false, false, false, -1, -1], + [false, false, false, -1, -1], + [false, false, kColSpanned, kColSpanned, -1], + [kRowSpanned, false, false, -1, -1], + [kRowSpanned, false, kRowSpanned, false, false], + ]; + + testTableSelection("tableinsane", cellsArray); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + </head> + <body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052" + title="Fix our nsHTMLAccessibleTable class so GetIndexAt and GetRowAtIndex and GetColumnAtIndex behave consistently"> + Mozilla Bug 410052 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=501635" + title="nsHTMLTableAccessible::GetSelectedCells contains index duplicates for spanned rows or columns"> + Mozilla Bug 501635 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=417929" + title="nsIAccessiblTable selectRows does not unselect previously selected rows"> + Mozilla Bug 417929 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=501659" + title="HTML table's isRowSelected/isColumnSelected shouldn't fail if row or column has cell holes"> + Mozilla Bug 501659 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Test Table --> + <br><br><b> Testing Table:</b><br><br> + <center> + <table cellpadding="2" cellspacing="2" border="1" width="50%" id="table"> + <tbody> + <tr> + <td><br></td> + <td><br></td> + <td rowspan="1" colspan="2"><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + <td rowspan="4" colspan="1"><br></td> + </tr> + <tr> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + </tr> + <tr> + <td><br></td> + <td rowspan="2" colspan="2">c1</td> + <td><br></td> + <td><br></td> + <td rowspan="2" colspan="1"><br></td> + <td><br></td> + </tr> + <tr> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + </tr> + </tbody> + </table> + + <table border="1" id="tableinsane"> + <thead> + <tr> + <th>col1</th> + <th>col2</th> + <th>col3</th> + </tr> + </thead> + <tbody> + <tr> + <td>1</td> + <td>2</td> + <td>3</td> + </tr> + <tr> + <td rowspan="3">4</td> + <td colspan="4">5</td> + </tr> + <tr> + <td>6</td> + <td rowspan="2">7</td> + </tr> + <tr> + <td>8</td> + <td>9</td> + <td>10</td> + </tr> + </tbody> + </table> + + </center> + </body> +</html> diff --git a/accessible/tests/mochitest/table/test_sels_tree.xhtml b/accessible/tests/mochitest/table/test_sels_tree.xhtml new file mode 100644 index 0000000000..254377b342 --- /dev/null +++ b/accessible/tests/mochitest/table/test_sels_tree.xhtml @@ -0,0 +1,78 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible Table selection tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../table.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; + + function doTest() + { + var cellsArray = + [ + [false, false], + [false, false], + [false, false] + ]; + + testTableSelection("tree", cellsArray); + testSelectTableRow("tree", 0, cellsArray); + testUnselectTableRow("tree", 0, cellsArray); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "tree", new nsTableTreeView(3)); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Mozilla Bug 503727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="debug"/> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + <treecol id="scol" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/table/test_struct_ariagrid.html b/accessible/tests/mochitest/table/test_struct_ariagrid.html new file mode 100644 index 0000000000..8541b6a9da --- /dev/null +++ b/accessible/tests/mochitest/table/test_struct_ariagrid.html @@ -0,0 +1,147 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Table accessible tree and table interface tests for ARIA grid</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // Pure ARIA grid + var cellsArray = [ + [kColHeaderCell, kColHeaderCell, kColHeaderCell], + [kRowHeaderCell, kDataCell, kDataCell], + [kRowHeaderCell, kDataCell, kDataCell], + ]; + + testTableStruct("table", cellsArray); + + // //////////////////////////////////////////////////////////////////////// + // HTML table based ARIA grid + cellsArray = [ + [kColHeaderCell, kColHeaderCell, kColHeaderCell], + [kDataCell, kDataCell, kDataCell], + [kDataCell, kDataCell, kDataCell], + ]; + + testTableStruct("grid", cellsArray); + + // //////////////////////////////////////////////////////////////////////// + // ARIA grid with HTML table elements + cellsArray = [ + [kColHeaderCell, kColHeaderCell], + [kDataCell, kDataCell], + ]; + + testTableStruct("grid2", cellsArray); + + // //////////////////////////////////////////////////////////////////////// + // ARIA grid of wrong markup + cellsArray = [ ]; + testTableStruct("grid3", cellsArray); + + cellsArray = [ [] ]; + testTableStruct("grid4", cellsArray); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + title="ARIA grid based on HTML table" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=491683">Mozilla Bug 491683</a> + <a target="_blank" + title="implement IAccessibleTable2" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424">Mozilla Bug 512424</a> + <a target="_blank" + title="nsHTMLTableCellAccessible is used in dojo's ARIA grid" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=513848">Mozilla Bug 513848</a> + <a target="_blank" + title="Crash [@ AccIterator::GetNext()]" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=675861">Mozilla Bug 675861</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Not usual markup to avoid text accessible between cell accessibles --> + <div id="table" role="grid"> + <div role="row"><span + id="table_ch_1" role="columnheader">col_1</span><span + id="table_ch_2" role="columnheader">col_2</span><span + id="table_ch_3" role="columnheader">col_3</span></div> + <div role="row"><span + id="table_rh_1" role="rowheader">row_1</span><span + id="table_dc_1" role="gridcell">cell1</span><span + id="table_dc_2" role="gridcell">cell2</span></div> + <div role="row"><span + id="table_rh_2" role="rowheader">row_2</span><span + id="table_dc_3" role="gridcell">cell3</span><span + id="table_dc_4" role="gridcell">cell4</span></div> + </div> + + <table role="grid" id="grid" border="1" cellpadding="10" cellspacing="0"> + <thead> + <tr role="row"> + <th role="columnheader">subject</td> + <th role="columnheader">sender</th> + <th role="columnheader">date</th> + </tr> + </thead> + <tbody> + <tr role="row"> + <td role="gridcell" tabindex="0">about everything</td> + <td role="gridcell">president</td> + <td role="gridcell">today</td> + </tr> + <tr role="row"> + <td role="gridcell">new bugs</td> + <td role="gridcell">mozilla team</td> + <td role="gridcell">today</td> + </tr> + </tbody> + </table> + + <!-- ARIA grid containing presentational HTML:table with HTML:td used as ARIA + grid cells (focusable and not focusable cells) --> + <div role="grid" id="grid2"> + <div role="row"> + <table role="presentation"> + <tr> + <td role="columnheader">header1</td> + <td role="columnheader">header2</td> + </tr> + </table> + </div> + <div role="row"> + <table role="presentation"> + <tr> + <td role="gridcell">cell1</td> + <td role="gridcell" tabindex="-1">cell2</td> + </tr> + </table> + </div> + </div> + + <!-- Wrong markup ARIA grid --> + <div role="grid" id="grid3"></div> + <div role="grid" id="grid4"><div role="row"></div></div> +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_struct_ariatreegrid.html b/accessible/tests/mochitest/table/test_struct_ariatreegrid.html new file mode 100644 index 0000000000..8d36a2b350 --- /dev/null +++ b/accessible/tests/mochitest/table/test_struct_ariatreegrid.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Table accessible tree and table interface tests for ARIA tree grid</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // HTML based ARIA tree grid + + var cellsArray = [ + [kColHeaderCell, kColHeaderCell, kColHeaderCell], + [kDataCell, kDataCell, kDataCell], + [kDataCell, kDataCell, kDataCell], + ]; + + testTableStruct("treegrid", cellsArray, kNoColumnHeader, "", "", + kTreeTable); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + title="ARIA treegrid role on HTML:table makes thead/tbody accessible" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=491683">Mozilla Bug 516133</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table role="treegrid" id="treegrid" + border="1" cellpadding="10" cellspacing="0"> + <thead> + <tr role="row"> + <th role="columnheader">subject</td> + <th role="columnheader">sender</th> + <th role="columnheader">date</th> + </tr> + </thead> + <tbody> + <tr role="row"> + <td role="gridcell">about everything</td> + <td role="gridcell">president</td> + <td role="gridcell">today</td> + </tr> + <tr role="row"> + <td role="gridcell">new bugs</td> + <td role="gridcell">mozilla team</td> + <td role="gridcell">today</td> + </tr> + </tbody> + </table> + +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_struct_table.html b/accessible/tests/mochitest/table/test_struct_table.html new file mode 100644 index 0000000000..46bad05c62 --- /dev/null +++ b/accessible/tests/mochitest/table/test_struct_table.html @@ -0,0 +1,217 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Table accessible tree and table interface tests for HTML tables</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../table.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // column headers from thead and tfoot + + cellsArray = [ + [kColHeaderCell, kColHeaderCell, kColSpanned], + [kRowSpanned, kColHeaderCell, kColHeaderCell], + [kDataCell, kDataCell, kDataCell], + [kColHeaderCell, kColHeaderCell, kColHeaderCell], + ]; + + testTableStruct("table1", cellsArray); + + // //////////////////////////////////////////////////////////////////////// + // row and column headers from thead and @scope + + var cellsArray = [ + [kColHeaderCell, kColHeaderCell, kColHeaderCell], + [kRowHeaderCell, kDataCell, kDataCell], + [kRowHeaderCell, kDataCell, kDataCell], + ]; + + testTableStruct("table2", cellsArray); + + // //////////////////////////////////////////////////////////////////////// + // caption and @summary + + cellsArray = [ + [kColHeaderCell, kColHeaderCell, kColHeaderCell, kColHeaderCell], + [kRowHeaderCell, kDataCell, kDataCell, kDataCell], + [kRowHeaderCell, kDataCell, kDataCell, kDataCell], + ]; + + testTableStruct("table3", cellsArray, kNoColumnHeader, + "Test Table", + "this is a test table for nsIAccessibleTable"); + + // //////////////////////////////////////////////////////////////////////// + // row and column spans + + cellsArray = [ + [kDataCell, kDataCell, kDataCell, kColSpanned, kDataCell, kDataCell, kDataCell, kDataCell], + [kDataCell, kDataCell, kDataCell, kDataCell, kDataCell, kDataCell, kDataCell, kRowSpanned], + [kDataCell, kDataCell, kColSpanned, kDataCell, kDataCell, kDataCell, kDataCell, kRowSpanned], + [kDataCell, kRowSpanned, kSpanned, kDataCell, kDataCell, kRowSpanned, kDataCell, kRowSpanned], + ]; + + testTableStruct("table4", cellsArray); + + // //////////////////////////////////////////////////////////////////////// + // Table with a cell that has display: block; style + + cellsArray = [ + [kRowHeaderCell, kDataCell], + ]; + + testTableStruct("table5", cellsArray); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Fix our nsHTMLAccessibleTable class so GetIndexAt and GetRowAtIndex and GetColumnAtIndex behave consistently" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052">Mozilla Bug 410052</a> + <a target="_blank" + title="GetCellDataAt callers that expect an error if no cell is found are wrong" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=417912">Mozilla Bug 417912</a> + <a target="_blank" + title="create accessibles for HTML tr" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=493695">Mozilla Bug 493695</a> + <a target="_blank" + title="implement IAccessibleTable2" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424">Mozilla Bug 512424</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table id="table1"> + <thead> + <tr> + <th rowspan="2">col1</th><th colspan="2">col2</th> + </tr> + <tr> + <th>col2sub1</th><th>col2sub2</th> + </tr> + </thead> + <tbody> + <tr> + <td>cell1</td><td>cell2</td><td>cell3</td> + </tr> + </tbody> + <tfoot> + <tr> + <th>col1</th><th>col2</th><th>col3</th> + </tr> + </tfoot> + </table> + + <table id="table2"> + <thead> + <tr> + <th id="table1_0">col1</th> + <th id="table1_1">col2</th> + <td id="table1_2" scope="col">col3</td> + </tr> + </thead> + <tbody> + <tr> + <th id="table1_3">row1</th> + <td id="table1_4">cell1</td> + <td id="table1_5">cell2</td> + </tr> + <tr> + <td id="table1_6" scope="row">row2</td> + <td id="table1_7">cell3</td> + <td id="table1_8">cell4</td> + </tr> + </tbody> + </table> + + <table id="table3" border="1" + summary="this is a test table for nsIAccessibleTable"> + <caption>Test Table</caption> + <thead> + <tr> + <th></th> + <th>columnHeader_1</th> + <th id ="col2a">columnHeader_2</th> + <th>columnHeader_3</th> + </tr> + </thead> + <tr> + <th id="row2a">rowHeader_1</th> + <td id="row2b">row1_column1</td> + <td id ="col2b">row1_column2</td> + <td id="row2c">row1_column3</td> + </tr> + <tr> + <th>rowHeader_2</th> + <td>row2_column1</td> + <td id ="col2c">row2_column2</td> + <td>row2_column3</td> + </tr> + </table> + + <table id="table4" cellpadding="2" cellspacing="2" border="1" width="50%"> + <tbody> + <tr> + <td><br></td> + <td><br></td> + <td rowspan="1" colspan="2"><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + <td rowspan="4" colspan="1"><br></td> + </tr> + <tr> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + </tr> + <tr> + <td><br></td> + <td rowspan="2" colspan="2">c1</td> + <td><br></td> + <td><br></td> + <td rowspan="2" colspan="1"><br></td> + <td><br></td> + </tr> + <tr> + <td><br></td> + <td><br></td> + <td><br></td> + <td><br></td> + </tr> + </tbody> + </table> + + <table id="table5"> + <tr> + <th>a</th> + <td style="display: block;">b</td> + </tr> + </table> + +</body> +</html> diff --git a/accessible/tests/mochitest/table/test_struct_tree.xhtml b/accessible/tests/mochitest/table/test_struct_tree.xhtml new file mode 100644 index 0000000000..6710bd2e8b --- /dev/null +++ b/accessible/tests/mochitest/table/test_struct_tree.xhtml @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Table accessible tree and table interface tests for XUL trees"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../table.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; + + function doTest() + { + var cellsArray = [ + [kDataCell, kDataCell], + [kDataCell, kDataCell], + [kDataCell, kDataCell] + ]; + + testTableStruct("table", cellsArray, kTreeColumnHeader); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yXULTreeLoadEvent(doTest, "table", new nsTableTreeView(3)); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=512424" + title="implement IAccessibleTable2"> + Mozilla Bug 512424 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="debug"/> + <tree id="table" flex="1"> + <treecols> + <treecol id="col" flex="1" label="column"/> + <treecol id="scol" flex="1" label="column 2"/> + </treecols> + <treechildren id="treechildren"/> + </tree> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/table/test_table_1.html b/accessible/tests/mochitest/table/test_table_1.html new file mode 100644 index 0000000000..b1331a5cc3 --- /dev/null +++ b/accessible/tests/mochitest/table/test_table_1.html @@ -0,0 +1,107 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + + <script type="application/javascript"> + +function doTest() { + var accTable = getAccessible("table", [nsIAccessibleTable]); + + var s = window.getSelection(); + if (s.rangeCount > 0) + s.removeAllRanges(); + + var cell = getNode("col2b"); + var range = document.createRange(); + range.selectNode(cell); + s.addRange(range); + + is(accTable.selectedCellCount, 1, "only one cell selected"); + cell = getNode("col2a"); + range = document.createRange(); + range.selectNode(cell); + s.addRange(range); + cell = getNode("col2c"); + range = document.createRange(); + range.selectNode(cell); + s.addRange(range); + is(accTable.selectedColumnCount, 1, "only one column selected"); + + cell = getNode("row2a"); + range = document.createRange(); + range.selectNode(cell); + s.addRange(range); + cell = getNode("row2b"); + range = document.createRange(); + range.selectNode(cell); + s.addRange(range); + range = document.createRange(); + cell = getNode("row2c"); + range.selectNode(cell); + s.addRange(range); + + is(accTable.selectedRowCount, 1, "no cells selected"); + + // These shouldn't throw. + try { + accTable.getColumnDescription(1); + accTable.getRowDescription(1); + } catch (ex) { + ok(false, "getColumnDescription/getRowDescription shouldn't throw."); + } + SimpleTest.finish(); +} +SimpleTest.waitForExplicitFinish(); +addA11yLoadEvent(doTest); + </script> + </head> + <body > + + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=410052">Mozilla Bug 410052</a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=760878" + title="decomtaminate Get Row / Column Description() on accessible tables"> + Mozilla Bug 760878 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Test Table --> + <br><br><b> Testing Table:</b><br><br> + <center> + <table id="table" border="1" + summary="this is a test table for nsIAccessibleTable" > + <caption>Test Table</caption> + <thead> + <tr> + <th></th> + <th>columnHeader_1</th> + <th id ="col2a">columnHeader_2</th> + <th>columnHeader_3</th> + </tr> + </thead> + <tr> + <th id="row2a">rowHeader_1</th> + <td id="row2b">row1_column1</td> + <td id ="col2b">row1_column2</td> + <td id="row2c">row1_column3</td> + </tr> + <tr> + <th>rowHeader_2</th> + <td>row2_column1</td> + <td id ="col2c">row2_column2</td> + <td>row2_column3</td> + </tr> + </table> + </center> + </body> +</html> diff --git a/accessible/tests/mochitest/table/test_table_2.html b/accessible/tests/mochitest/table/test_table_2.html new file mode 100644 index 0000000000..6bd7c56b37 --- /dev/null +++ b/accessible/tests/mochitest/table/test_table_2.html @@ -0,0 +1,87 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> + <head> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="text/javascript"> + +function doTest() { + // Test table with role=alert. + var tableInterfaceExposed = true; + var accTable3 = getAccessible("table3", [nsIAccessibleTable], null, DONOTFAIL_IF_NO_INTERFACE); + if (!accTable3) + tableInterfaceExposed = false; + ok(tableInterfaceExposed, "table interface is not exposed"); + + if (tableInterfaceExposed) { + testRole(accTable3, ROLE_ALERT); + + is(accTable3.getCellAt(0, 0).firstChild.name, "cell0", "wrong cell"); + is(accTable3.getCellAt(0, 1).firstChild.name, "cell1", "wrong cell"); + } + + // Test table with role=log and aria property in tr. We create accessible for + // tr in this case. + tableInterfaceExposed = true; + var accTable4 = getAccessible("table4", [nsIAccessibleTable], null, DONOTFAIL_IF_NO_INTERFACE); + if (!accTable4) + tableInterfaceExposed = false; + ok(tableInterfaceExposed, "table interface is not exposed"); + + if (tableInterfaceExposed) { + let accNotCreated = (!isAccessible("tr")); + ok(!accNotCreated, "missed tr accessible"); + + testRole(accTable4, ROLE_TABLE); + + is(accTable4.getCellAt(0, 0).firstChild.name, "cell0", "wrong cell"); + is(accTable4.getCellAt(0, 1).firstChild.name, "cell1", "wrong cell"); + is(accTable4.getCellAt(1, 0).firstChild.name, "cell2", "wrong cell"); + is(accTable4.getCellAt(1, 1).firstChild.name, "cell3", "wrong cell"); + } + + SimpleTest.finish(); +} +SimpleTest.waitForExplicitFinish(); +addA11yLoadEvent(doTest); + </script> + </head> + + <body > + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=419811">Mozilla Bug 419811</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Test Table --> + <br><br><b> Testing Table:</b><br><br> + <center> + <table id="table3" border="1" role="alert"> + <tr> + <td>cell0</td> + <td>cell1</td> + </tr> + </table> + + <table id="table4" border="1" role="log"> + <tr aria-live="polite" id="tr"> + <td>cell0</td> + <td>cell1</td> + </tr> + <tr> + <td>cell2</td> + <td>cell3</td> + </tr> + </table> + + </center> + </body> +</html> diff --git a/accessible/tests/mochitest/table/test_table_mutation.html b/accessible/tests/mochitest/table/test_table_mutation.html new file mode 100644 index 0000000000..671e627244 --- /dev/null +++ b/accessible/tests/mochitest/table/test_table_mutation.html @@ -0,0 +1,100 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> + <title>Table mutation</title> + <meta http-equiv="content-type" content="text/html; charset=UTF-8"> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../table.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + + async function doTest() { + let headers = [ + { + cell: "t1r1c1", + columnHeaderCells: [], + rowHeaderCells: [], + }, + // t1r2 is hidden + { + cell: "t1r3c1", + columnHeaderCells: ["t1r1c1"], + rowHeaderCells: [], + }, + ]; + testHeaderCells(headers); + + info("Remove row"); + let reordered = waitForEvent(EVENT_REORDER, "t1"); + getNode("t1r1").hidden = true; + await reordered; + headers = [ + // t1r1 and t1r2 are hidden + { + cell: "t1r3c1", + columnHeaderCells: [], + rowHeaderCells: [], + }, + ]; + testHeaderCells(headers); + + info("Add rows"); + reordered = waitForEvent(EVENT_REORDER, "t1"); + getNode("t1r1").hidden = false; + getNode("t1r2").hidden = false; + await reordered; + headers = [ + { + cell: "t1r1c1", + columnHeaderCells: [], + rowHeaderCells: [], + }, + { + cell: "t1r2c1", + columnHeaderCells: ["t1r1c1"], + rowHeaderCells: [], + }, + { + cell: "t1r3c1", + columnHeaderCells: ["t1r2c1", "t1r1c1"], + rowHeaderCells: [], + }, + ]; + testHeaderCells(headers); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table id="t1"> + <tr id="t1r1"> + <th id="t1r1c1"></th> + </tr> + <tr id="t1r2" hidden> + <th id="t1r2c1"></th> + </tr> + <tr id="t1r3"> + <td id="t1r3c1"></td> + </tr> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/test_OuterDocAccessible.html b/accessible/tests/mochitest/test_OuterDocAccessible.html new file mode 100644 index 0000000000..b7a719aba5 --- /dev/null +++ b/accessible/tests/mochitest/test_OuterDocAccessible.html @@ -0,0 +1,87 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=441519 +--> +<head> + <title>nsOuterDocAccessible chrome tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="common.js"></script> + <script type="application/javascript" + src="states.js"></script> + <script type="application/javascript" + src="role.js"></script> + + <script type="application/javascript"> + // needed error return value + const ns_error_invalid_arg = Cr.NS_ERROR_INVALID_ARG; + + function doTest() { + // Get accessible for body tag. + var docAcc = getAccessible(document); + + if (docAcc) { + var outerDocAcc = getAccessible(docAcc.parent); + + if (outerDocAcc) { + testRole(outerDocAcc, ROLE_INTERNAL_FRAME); + + // check if it is focusable. + testStates(outerDocAcc, STATE_FOCUSABLE, 0); + + // see bug 428954: No name wanted for internal frame + is(outerDocAcc.name, null, "Wrong name for internal frame!"); + + // see bug 440770, no actions wanted on outer doc + is(outerDocAcc.actionCount, 0, + "Wrong number of actions for internal frame!"); + + try { + outerDocAcc.getActionName(0); + throw new Error("No exception thrown for actionName!"); + } catch (e) { + is(e.result, ns_error_invalid_arg, + "Wrong return value for actionName call!"); + } + + try { + outerDocAcc.getActionDescription(0); + throw new Error("No exception thrown for actionDescription!"); + } catch (e) { + is(e.result, ns_error_invalid_arg, + "Wrong return value for actionDescription call!"); + } + + try { + outerDocAcc.doAction(0); + throw new Error("No exception thrown for doAction!"); + } catch (e) { + is(e.result, ns_error_invalid_arg, + "Wrong return value for doAction call!"); + } + } + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=441519" + title="nsOuterDocAccessible chrome tests"> + Mozilla Bug 441519 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/test_aria_token_attrs.html b/accessible/tests/mochitest/test_aria_token_attrs.html new file mode 100644 index 0000000000..ad0bc25970 --- /dev/null +++ b/accessible/tests/mochitest/test_aria_token_attrs.html @@ -0,0 +1,417 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=452388 +--> +<head> + <title>An NMTOKEN based ARIA property is undefined if the ARIA attribute is not present, or is set to "" or "undefined"</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="common.js"></script> + <script type="application/javascript" + src="role.js"></script> + <script type="application/javascript" + src="states.js"></script> + + <script type="application/javascript"> + function doTest() { + // test aria-pressed state mapping to roles PUSHBUTTON vs TOGGLEBUTTON + testRole("button_pressed_true", ROLE_TOGGLE_BUTTON); + testRole("button_pressed_false", ROLE_TOGGLE_BUTTON); + testRole("button_pressed_empty", ROLE_PUSHBUTTON); + testRole("button_pressed_undefined", ROLE_PUSHBUTTON); + testRole("button_pressed_absent", ROLE_PUSHBUTTON); + + // test button aria-pressed states + testStates("button_pressed_true", STATE_PRESSED, 0, STATE_CHECKABLE); + testStates("button_pressed_false", 0, 0, STATE_CHECKABLE | STATE_PRESSED); + testStates("button_pressed_empty", 0, 0, STATE_PRESSED | STATE_CHECKABLE); + testStates("button_pressed_undefined", 0, 0, STATE_PRESSED | STATE_CHECKABLE); + testStates("button_pressed_absent", 0, 0, STATE_PRESSED | STATE_CHECKABLE); + + // test (checkbox) checkable and checked states + testStates("checkbox_checked_true", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("checkbox_checked_mixed", (STATE_CHECKABLE | STATE_MIXED), 0, STATE_CHECKED); + testStates("checkbox_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("checkbox_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("checkbox_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("checkbox_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED); + + // test native checkbox checked state and aria-checked state (if conflict, native wins) + testStates("native_checkbox_nativechecked_ariatrue", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("native_checkbox_nativechecked_ariafalse", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("native_checkbox_nativechecked_ariaempty", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("native_checkbox_nativechecked_ariaundefined", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("native_checkbox_nativechecked_ariaabsent", (STATE_CHECKABLE | STATE_CHECKED)); + + testStates("native_checkbox_nativeunchecked_ariatrue", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("native_checkbox_nativeunchecked_ariafalse", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("native_checkbox_nativeunchecked_ariaempty", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("native_checkbox_nativeunchecked_ariaundefined", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("native_checkbox_nativeunchecked_ariaabsent", STATE_CHECKABLE, 0, STATE_CHECKED); + + // test (checkbox) readonly states + testStates("checkbox_readonly_true", STATE_READONLY); + testStates("checkbox_readonly_false", 0, 0, STATE_READONLY); + testStates("checkbox_readonly_empty", 0, 0, STATE_READONLY); + testStates("checkbox_readonly_undefined", 0, 0, STATE_READONLY); + testStates("checkbox_readonly_absent", 0, 0, STATE_READONLY); + + // test (checkbox) required states + testStates("checkbox_required_true", STATE_REQUIRED); + testStates("checkbox_required_false", 0, 0, STATE_REQUIRED); + testStates("checkbox_required_empty", 0, 0, STATE_REQUIRED); + testStates("checkbox_required_undefined", 0, 0, STATE_REQUIRED); + testStates("checkbox_required_absent", 0, 0, STATE_REQUIRED); + + // test (checkbox) invalid states + testStates("checkbox_invalid_true", STATE_INVALID); + testStates("checkbox_invalid_false", 0, 0, STATE_INVALID); + testStates("checkbox_invalid_empty", 0, 0, STATE_INVALID); + testStates("checkbox_invalid_undefined", 0, 0, STATE_INVALID); + testStates("checkbox_invalid_absent", 0, 0, STATE_INVALID); + + // test (checkbox) disabled states + testStates("checkbox_disabled_true", STATE_UNAVAILABLE); + testStates("checkbox_disabled_false", 0, 0, STATE_UNAVAILABLE); + testStates("checkbox_disabled_empty", 0, 0, STATE_UNAVAILABLE); + testStates("checkbox_disabled_undefined", 0, 0, STATE_UNAVAILABLE); + testStates("checkbox_disabled_absent", 0, 0, STATE_UNAVAILABLE); + + // test (listbox) multiselectable states + testStates("listbox_multiselectable_true", STATE_MULTISELECTABLE | STATE_EXTSELECTABLE); + testStates("listbox_multiselectable_false", 0, 0, STATE_MULTISELECTABLE | STATE_EXTSELECTABLE); + testStates("listbox_multiselectable_empty", 0, 0, STATE_MULTISELECTABLE | STATE_EXTSELECTABLE); + testStates("listbox_multiselectable_undefined", 0, 0, STATE_MULTISELECTABLE | STATE_EXTSELECTABLE); + testStates("listbox_multiselectable_absent", 0, 0, STATE_MULTISELECTABLE | STATE_EXTSELECTABLE); + + // test (option) checkable and checked states + testStates("option_checked_true", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("option_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("option_checked_empty", 0, 0, STATE_CHECKABLE | STATE_CHECKED); + testStates("option_checked_undefined", 0, 0, STATE_CHECKABLE | STATE_CHECKED); + testStates("option_checked_absent", 0, 0, STATE_CHECKABLE | STATE_CHECKED); + + // test (menuitem) checkable and checked states, which are unsupported on this role + testStates("menuitem_checked_true", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); + testStates("menuitem_checked_mixed", 0, 0, (STATE_CHECKABLE | STATE_CHECKED | STATE_MIXED)); + testStates("menuitem_checked_false", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); + testStates("menuitem_checked_empty", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); + testStates("menuitem_checked_undefined", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); + testStates("menuitem_checked_absent", 0, 0, (STATE_CHECKABLE | STATE_CHECKED)); + + // test (menuitemcheckbox) checkable and checked states + testStates("menuitemcheckbox_checked_true", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("menuitemcheckbox_checked_mixed", (STATE_CHECKABLE | STATE_MIXED), 0, STATE_CHECKED); + testStates("menuitemcheckbox_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("menuitemcheckbox_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("menuitemcheckbox_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("menuitemcheckbox_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED); + + // test (menuitemcheckbox) readonly states + testStates("menuitemcheckbox_readonly_true", STATE_READONLY); + testStates("menuitemcheckbox_readonly_false", 0, 0, STATE_READONLY); + testStates("menuitemcheckbox_readonly_empty", 0, 0, STATE_READONLY); + testStates("menuitemcheckbox_readonly_undefined", 0, 0, STATE_READONLY); + testStates("menuitemcheckbox_readonly_absent", 0, 0, STATE_READONLY); + + // test (menuitemradio) checkable and checked states + testStates("menuitemradio_checked_true", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("menuitemradio_checked_mixed", STATE_CHECKABLE, 0, (STATE_MIXED | STATE_CHECKED)); + testStates("menuitemradio_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("menuitemradio_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("menuitemradio_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("menuitemradio_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED); + + // test (menuitemradio) readonly states + testStates("menuitemradio_readonly_true", STATE_READONLY); + testStates("menuitemradio_readonly_false", 0, 0, STATE_READONLY); + testStates("menuitemradio_readonly_empty", 0, 0, STATE_READONLY); + testStates("menuitemradio_readonly_undefined", 0, 0, STATE_READONLY); + testStates("menuitemradio_readonly_absent", 0, 0, STATE_READONLY); + + // test (radio) checkable and checked states + testStates("radio_checked_true", (STATE_CHECKABLE | STATE_CHECKED)); + testStates("radio_checked_mixed", STATE_CHECKABLE, 0, (STATE_MIXED | STATE_CHECKED)); + testStates("radio_checked_false", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("radio_checked_empty", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("radio_checked_undefined", STATE_CHECKABLE, 0, STATE_CHECKED); + testStates("radio_checked_absent", STATE_CHECKABLE, 0, STATE_CHECKED); + + // test (radiogroup) readonly states + testStates("radiogroup_readonly_true", STATE_READONLY); + testStates("radiogroup_readonly_false", 0, 0, STATE_READONLY); + testStates("radiogroup_readonly_empty", 0, 0, STATE_READONLY); + testStates("radiogroup_readonly_undefined", 0, 0, STATE_READONLY); + testStates("radiogroup_readonly_absent", 0, 0, STATE_READONLY); + + // test (switch) readonly states + testStates("switch_readonly_true", STATE_READONLY); + testStates("switch_readonly_false", 0, 0, STATE_READONLY); + testStates("switch_readonly_empty", 0, 0, STATE_READONLY); + testStates("switch_readonly_undefined", 0, 0, STATE_READONLY); + testStates("switch_readonly_absent", 0, 0, STATE_READONLY); + + // test (textbox) multiline states + testStates("textbox_multiline_true", 0, EXT_STATE_MULTI_LINE); + testStates("textbox_multiline_false", 0, EXT_STATE_SINGLE_LINE); + testStates("textbox_multiline_empty", 0, EXT_STATE_SINGLE_LINE); + testStates("textbox_multiline_undefined", 0, EXT_STATE_SINGLE_LINE); + testStates("textbox_multiline_absent", 0, EXT_STATE_SINGLE_LINE); + + // test (textbox) readonly states + testStates("textbox_readonly_true", STATE_READONLY); + testStates("textbox_readonly_false", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("textbox_readonly_empty", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("textbox_readonly_undefined", 0, EXT_STATE_EDITABLE, STATE_READONLY); + testStates("textbox_readonly_absent", 0, EXT_STATE_EDITABLE, STATE_READONLY); + + // test native textbox readonly state and aria-readonly state (if conflict, native wins) + testStates("native_textbox_nativereadonly_ariatrue", STATE_READONLY); + testStates("native_textbox_nativereadonly_ariafalse", STATE_READONLY); + testStates("native_textbox_nativereadonly_ariaempty", STATE_READONLY); + testStates("native_textbox_nativereadonly_ariaundefined", STATE_READONLY); + testStates("native_textbox_nativereadonly_ariaabsent", STATE_READONLY); + + testStates("native_textbox_nativeeditable_ariatrue", 0, 0, STATE_READONLY); + testStates("native_textbox_nativeeditable_ariafalse", 0, 0, STATE_READONLY); + testStates("native_textbox_nativeeditable_ariaempty", 0, 0, STATE_READONLY); + testStates("native_textbox_nativeeditable_ariaundefined", 0, 0, STATE_READONLY); + testStates("native_textbox_nativeeditable_ariaabsent", 0, 0, STATE_READONLY); + + // test (treeitem) selectable and selected states + testStates("treeitem_selected_true", (STATE_SELECTABLE | STATE_SELECTED)); + testStates("treeitem_selected_false", STATE_SELECTABLE, 0, STATE_SELECTED); + testStates("treeitem_selected_empty", STATE_SELECTABLE, 0, STATE_SELECTED); + testStates("treeitem_selected_undefined", STATE_SELECTABLE, 0, STATE_SELECTED); + testStates("treeitem_selected_absent", STATE_SELECTABLE, 0, STATE_SELECTED); + + // test (treeitem) haspopup states + testStates("treeitem_haspopup_true", STATE_HASPOPUP); + testStates("treeitem_haspopup_false", 0, 0, STATE_HASPOPUP); + testStates("treeitem_haspopup_empty", 0, 0, STATE_HASPOPUP); + testStates("treeitem_haspopup_undefined", 0, 0, STATE_HASPOPUP); + testStates("treeitem_haspopup_absent", 0, 0, STATE_HASPOPUP); + + // test (treeitem) expandable and expanded/collapsed states + testStates("treeitem_expanded_true", STATE_EXPANDED, EXT_STATE_EXPANDABLE); + testStates("treeitem_expanded_false", STATE_COLLAPSED, EXT_STATE_EXPANDABLE); + testStates("treeitem_expanded_empty", 0, 0, STATE_EXPANDED | STATE_COLLAPSED, EXT_STATE_EXPANDABLE); + testStates("treeitem_expanded_undefined", 0, 0, STATE_EXPANDED | STATE_COLLAPSED, EXT_STATE_EXPANDABLE); + testStates("treeitem_expanded_absent", 0, 0, STATE_EXPANDED | STATE_COLLAPSED, EXT_STATE_EXPANDABLE); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=452388"> + Mozilla Bug 452388 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=499653" + title="Unify ARIA state attributes mapping rules"> + Mozilla Bug 499653 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=989958" + title="Pressed state is not exposed on a button element with aria-pressed attribute" + Mozilla Bug 989958 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="button_pressed_true" role="button" aria-pressed="true">This button has aria-pressed="true" and should get ROLE_TOGGLE_BUTTON. It should also get STATE_PRESSED.</div> + <div id="button_pressed_false" role="button" aria-pressed="false">This button has aria-pressed="false" and should get ROLE_TOGGLE_BUTTON.</div> + <div id="button_pressed_empty" role="button" aria-pressed="">This button has aria-pressed="" and should <emph>not</emph> get ROLE_BUTTON.</div> + <div id="button_pressed_undefined" role="button" aria-pressed="undefined">This button has aria-pressed="undefined" and should <emph>not</emph> get ROLE_TOGGLE_BUTTON.</div> + <div id="button_pressed_absent" role="button">This button has <emph>no</emph> aria-pressed attribute and should <emph>not</emph> get ROLE_TOGGLE_BUTTON.</div> + + <div id="checkbox_checked_true" role="checkbox" aria-checked="true">This checkbox has aria-checked="true" and should get STATE_CHECKABLE. It should also get STATE_checked.</div> + <div id="checkbox_checked_mixed" role="checkbox" aria-checked="mixed">This checkbox has aria-checked="mixed" and should get STATE_CHECKABLE. It should also get STATE_MIXED.</div> + <div id="checkbox_checked_false" role="checkbox" aria-checked="false">This checkbox has aria-checked="false" and should get STATE_CHECKABLE.</div> + <div id="checkbox_checked_empty" role="checkbox" aria-checked="">This checkbox has aria-checked="" and should <emph>not</emph> get STATE_CHECKABLE.</div> + <div id="checkbox_checked_undefined" role="checkbox" aria-checked="undefined">This checkbox has aria-checked="undefined" and should <emph>not</emph> get STATE_CHECKABLE.</div> + <div id="checkbox_checked_absent" role="checkbox">This checkbox has <emph>no</emph> aria-checked attribute and should get STATE_CHECKABLE.</div> + + <form action=""> + <input id="native_checkbox_nativechecked_ariatrue" type="checkbox" checked="checked" aria-checked="true"/> + <input id="native_checkbox_nativechecked_ariafalse" type="checkbox" checked="checked" aria-checked="false"/> + <input id="native_checkbox_nativechecked_ariaempty" type="checkbox" checked="checked" aria-checked=""/> + <input id="native_checkbox_nativechecked_ariaundefined" type="checkbox" checked="checked" aria-checked="undefined"/> + <input id="native_checkbox_nativechecked_ariaabsent" type="checkbox" checked="checked"/> + + <input id="native_checkbox_nativeunchecked_ariatrue" type="checkbox" aria-checked="true"/> + <input id="native_checkbox_nativeunchecked_ariafalse" type="checkbox" aria-checked="false"/> + <input id="native_checkbox_nativeunchecked_ariaempty" type="checkbox" aria-checked=""/> + <input id="native_checkbox_nativeunchecked_ariaundefined" type="checkbox" aria-checked="undefined"/> + <input id="native_checkbox_nativeunchecked_ariaabsent" type="checkbox"/> + </form> + + <div id="checkbox_readonly_true" role="checkbox" aria-readonly="true">This checkbox has aria-readonly="true" and should get STATE_READONLY.</div> + <div id="checkbox_readonly_false" role="checkbox" aria-readonly="false">This checkbox has aria-readonly="false" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="checkbox_readonly_empty" role="checkbox" aria-readonly="">This checkbox has aria-readonly="" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="checkbox_readonly_undefined" role="checkbox" aria-readonly="undefined">This checkbox has aria-readonly="undefined" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="checkbox_readonly_absent" role="checkbox">This checkbox has <emph>no</emph> aria-readonly attribute and should <emph>not</emph> get STATE_READONLY.</div> + + <div id="checkbox_required_true" role="checkbox" aria-required="true">This checkbox has aria-required="true" and should get STATE_REQUIRED.</div> + <div id="checkbox_required_false" role="checkbox" aria-required="false">This checkbox has aria-required="false" and should <emph>not</emph> get STATE_REQUIRED.</div> + <div id="checkbox_required_empty" role="checkbox" aria-required="">This checkbox has aria-required="" and should <emph>not</emph> get STATE_REQUIRED.</div> + <div id="checkbox_required_undefined" role="checkbox" aria-required="undefined">This checkbox has aria-required="undefined" and should <emph>not</emph> get STATE_REQUIRED.</div> + <div id="checkbox_required_absent" role="checkbox">This checkbox has <emph>no</emph> aria-required attribute and should <emph>not</emph> get STATE_REQUIRED.</div> + + <div id="checkbox_invalid_true" role="checkbox" aria-invalid="true">This checkbox has aria-invalid="true" and should get STATE_INVALID.</div> + <div id="checkbox_invalid_false" role="checkbox" aria-invalid="false">This checkbox has aria-invalid="false" and should <emph>not</emph> get STATE_INVALID.</div> + <div id="checkbox_invalid_empty" role="checkbox" aria-invalid="">This checkbox has aria-invalid="" and should <emph>not</emph> get STATE_INVALID.</div> + <div id="checkbox_invalid_undefined" role="checkbox" aria-invalid="undefined">This checkbox has aria-invalid="undefined" and should <emph>not</emph> get STATE_INVALID.</div> + <div id="checkbox_invalid_absent" role="checkbox">This checkbox has <emph>no</emph> aria-invalid attribute and should <emph>not</emph> get STATE_INVALID.</div> + + <div id="checkbox_disabled_true" role="checkbox" aria-disabled="true" tabindex="0">This checkbox has aria-disabled="true" and should get STATE_DISABLED.</div> + <div id="checkbox_disabled_false" role="checkbox" aria-disabled="false">This checkbox has aria-disabled="false" and should <emph>not</emph> get STATE_DISABLED.</div> + <div id="checkbox_disabled_empty" role="checkbox" aria-disabled="">This checkbox has aria-disabled="" and should <emph>not</emph> get STATE_DISABLED.</div> + <div id="checkbox_disabled_undefined" role="checkbox" aria-disabled="undefined">This checkbox has aria-disabled="undefined" and should <emph>not</emph> get STATE_DISABLED.</div> + <div id="checkbox_disabled_absent" role="checkbox">This checkbox has <emph>no</emph> aria-disabled attribute and should <emph>not</emph> get STATE_DISABLED.</div> + + <div id="listbox_multiselectable_true" role="listbox" aria-multiselectable="true"> + <div id="option_checked_true" role="option" aria-checked="true">item</div> + </div> + <div id="listbox_multiselectable_false" role="listbox" aria-multiselectable="false"> + <div id="option_checked_false" role="option" aria-checked="false">item</div> + </div> + <div id="listbox_multiselectable_empty" role="listbox" aria-multiselectable=""> + <div id="option_checked_empty" role="option" aria-checked="">item</div> + </div> + <div id="listbox_multiselectable_undefined" role="listbox" aria-multiselectable="undefined"> + <div id="option_checked_undefined" role="option" aria-checked="undefined">item</div> + </div> + <div id="listbox_multiselectable_absent" role="listbox"> + <div id="option_checked_absent" role="option">item</div> + </div> + + <div role="menu"> + <div id="menuitem_checked_true" role="menuitem" aria-checked="true">Generic menuitems don't support aria-checked.</div> + <div id="menuitem_checked_mixed" role="menuitem" aria-checked="mixed">Generic menuitems don't support aria-checked.</div> + <div id="menuitem_checked_false" role="menuitem" aria-checked="false">Generic menuitems don't support aria-checked.</div> + <div id="menuitem_checked_empty" role="menuitem" aria-checked="">Generic menuitems don't support aria-checked.</div> + <div id="menuitem_checked_undefined" role="menuitem" aria-checked="undefined">Generic menuitems don't support aria-checked.</div> + <div id="menuitem_checked_absent" role="menuitem">Generic menuitems don't support aria-checked.</div> + + <div id="menuitemcheckbox_checked_true" role="menuitemcheckbox" aria-checked="true">This menuitemcheckbox has aria-checked="true" and should get STATE_CHECKABLE. It should also get STATE_checked.</div> + <div id="menuitemcheckbox_checked_mixed" role="menuitemcheckbox" aria-checked="mixed">This menuitemcheckbox has aria-checked="mixed" and should get STATE_CHECKABLE. It should also get STATE_MIXED.</div> + <div id="menuitemcheckbox_checked_false" role="menuitemcheckbox" aria-checked="false">This menuitemcheckbox has aria-checked="false" and should get STATE_CHECKABLE.</div> + <div id="menuitemcheckbox_checked_empty" role="menuitemcheckbox" aria-checked="">This menuitemcheckbox has aria-checked="" and should <emph>not</emph> get STATE_CHECKABLE.</div> + <div id="menuitemcheckbox_checked_undefined" role="menuitemcheckbox" aria-checked="undefined">This menuitemcheckbox has aria-checked="undefined" and should <emph>not</emph> get STATE_CHECKABLE.</div> + <div id="menuitemcheckbox_checked_absent" role="menuitemcheckbox">This menuitemcheckbox has <emph>no</emph> aria-checked attribute and should <emph>not</emph> get STATE_CHECKABLE.</div> + + <div id="menuitemcheckbox_readonly_true" role="menuitemcheckbox" aria-readonly="true">This menuitemcheckbox has aria-readonly="true" and should get STATE_READONLY.</div> + <div id="menuitemcheckbox_readonly_false" role="menuitemcheckbox" aria-readonly="false">This menuitemcheckbox has aria-readonly="false" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="menuitemcheckbox_readonly_empty" role="menuitemcheckbox" aria-readonly="">This menuitemcheckbox has aria-readonly="" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="menuitemcheckbox_readonly_undefined" role="menuitemcheckbox" aria-readonly="undefined">This menuitemcheckbox has aria-readonly="undefined" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="menuitemcheckbox_readonly_absent" role="menuitemcheckbox">This menuitemcheckbox has <emph>no</emph> aria-readonly attribute and should <emph>not</emph> get STATE_READONLY.</div> + + <div id="menuitemradio_checked_true" role="menuitemradio" aria-checked="true">This menuitem has aria-checked="true" and should get STATE_CHECKABLE. It should also get STATE_checked.</div> + <div id="menuitemradio_checked_mixed" role="menuitemradio" aria-checked="mixed">This menuitem has aria-checked="mixed" and should get STATE_CHECKABLE. It should not get STATE_MIXED.</div> + <div id="menuitemradio_checked_false" role="menuitemradio" aria-checked="false">This menuitem has aria-checked="false" and should get STATE_CHECKABLE.</div> + <div id="menuitemradio_checked_empty" role="menuitemradio" aria-checked="">This menuitem has aria-checked="" and should <emph>not</emph> get STATE_CHECKABLE.</div> + <div id="menuitemradio_checked_undefined" role="menuitemradio" aria-checked="undefined">This menuitem has aria-checked="undefined" and should <emph>not</emph> get STATE_CHECKABLE.</div> + <div id="menuitemradio_checked_absent" role="menuitemradio">This menuitem has <emph>no</emph> aria-checked attribute but should get STATE_CHECKABLE.</div> + </div> + + <div id="menuitemradio_readonly_true" role="menuitemradio" aria-readonly="true">This menuitemradio has aria-readonly="true" and should get STATE_READONLY.</div> + <div id="menuitemradio_readonly_false" role="menuitemradio" aria-readonly="false">This menuitemradio has aria-readonly="false" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="menuitemradio_readonly_empty" role="menuitemradio" aria-readonly="">This menuitemradio has aria-readonly="" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="menuitemradio_readonly_undefined" role="menuitemradio" aria-readonly="undefined">This menuitemradio has aria-readonly="undefined" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="menuitemradio_readonly_absent" role="menuitemradio">This menuitemradio has <emph>no</emph> aria-readonly attribute and should <emph>not</emph> get STATE_READONLY.</div> + + <div id="radio_checked_true" role="radio" aria-checked="true">This menuitem has aria-checked="true" and should get STATE_CHECKABLE. It should also get STATE_CHECKED.</div> + <div id="radio_checked_mixed" role="radio" aria-checked="mixed">This radio button has aria-checked="mixed" and should get STATE_CHECKABLE. It should not get STATE_MIXED.</div> + <div id="radio_checked_false" role="radio" aria-checked="false">This menuitem has aria-checked="false" and should get STATE_CHECKABLE.</div> + <div id="radio_checked_empty" role="radio" aria-checked="">This menuitem has aria-checked="" and should <emph>not</emph> get STATE_CHECKABLE.</div> + <div id="radio_checked_undefined" role="radio" aria-checked="undefined">This menuitem has aria-checked="undefined" and should <emph>not</emph> get STATE_CHECKABLE.</div> + <div id="radio_checked_absent" role="radio">This menuitem has <emph>no</emph> aria-checked attribute but should get STATE_CHECKABLE.</div> + + <div id="radiogroup_readonly_true" role="radiogroup" aria-readonly="true"> + <div role="radio">yes</div> + <div role="radio">no</div> + </div> + <div id="radiogroup_readonly_false" role="radiogroup" aria-readonly="false"> + <div role="radio">yes</div> + <div role="radio">no</div> + </div> + <div id="radiogroup_readonly_empty" role="radiogroup" aria-readonly=""> + <div role="radio">yes</div> + <div role="radio">no</div> + </div> + <div id="radiogroup_readonly_undefined" role="radiogroup" aria-readonly="undefined"> + <div role="radio">yes</div> + <div role="radio">no</div> + </div> + <div id="radiogroup_readonly_absent" role="radiogroup"> + <div role="radio">yes</div> + <div role="radio">no</div> + </div> + + <div id="switch_readonly_true" role="switch" aria-readonly="true">This switch has aria-readonly="true" and should get STATE_READONLY.</div> + <div id="switch_readonly_false" role="switch" aria-readonly="false">This switch has aria-readonly="false" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="switch_readonly_empty" role="switch" aria-readonly="">This switch has aria-readonly="" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="switch_readonly_undefined" role="switch" aria-readonly="undefined">This switch has aria-readonly="undefined" and should <emph>not</emph> get STATE_READONLY.</div> + <div id="switch_readonly_absent" role="switch">This switch has <emph>no</emph> aria-readonly attribute and should <emph>not</emph> get STATE_READONLY.</div> + + <div id="textbox_readonly_true" role="textbox" aria-readonly="true"></div> + <div id="textbox_readonly_false" role="textbox" aria-readonly="false"></div> + <div id="textbox_readonly_empty" role="textbox" aria-readonly=""></div> + <div id="textbox_readonly_undefined" role="textbox" aria-readonly="undefined"></div> + <div id="textbox_readonly_absent" role="textbox"></div> + + <div id="textbox_multiline_true" role="textbox" aria-multiline="true"></div> + <div id="textbox_multiline_false" role="textbox" aria-multiline="false"></div> + <div id="textbox_multiline_empty" role="textbox" aria-multiline=""></div> + <div id="textbox_multiline_undefined" role="textbox" aria-multiline="undefined"></div> + <div id="textbox_multiline_absent" role="textbox"></div> + + <form action=""> + <input id="native_textbox_nativereadonly_ariatrue" readonly="readonly" aria-readonly="true"/> + <input id="native_textbox_nativereadonly_ariafalse" readonly="readonly" aria-readonly="false"/> + <input id="native_textbox_nativereadonly_ariaempty" readonly="readonly" aria-readonly=""/> + <input id="native_textbox_nativereadonly_ariaundefined" readonly="readonly" aria-readonly="undefined"/> + <input id="native_textbox_nativereadonly_ariaabsent" readonly="readonly"/> + + <input id="native_textbox_nativeeditable_ariatrue" aria-readonly="true"/> + <input id="native_textbox_nativeeditable_ariafalse" aria-readonly="false"/> + <input id="native_textbox_nativeeditable_ariaempty" aria-readonly=""/> + <input id="native_textbox_nativeeditable_ariaundefined" aria-readonly="undefined"/> + <input id="native_textbox_nativeeditable_ariaabsent"/> + </form> + + <div role="tree"> + <div id="treeitem_selected_true" role="treeitem" aria-selected="true">This treeitem has aria-selected="true" and should get STATE_SELECTABLE. It should also get STATE_SELECTED.</div> + <div id="treeitem_selected_false" role="treeitem" aria-selected="false">This treeitem has aria-selected="false" and should get STATE_SELECTABLE.</div> + <div id="treeitem_selected_empty" role="treeitem" aria-selected="">This treeitem has aria-selected="" and should <emph>not</emph> get STATE_SELECTABLE.</div> + <div id="treeitem_selected_undefined" role="treeitem" aria-selected="undefined">This treeitem has aria-selected="undefined" and should <emph>not</emph> get STATE_SELECTABLE.</div> + <div id="treeitem_selected_absent" role="treeitem">This treeitem has <emph>no</emph> aria-selected attribute and should <emph>not</emph> get STATE_SELECTABLE.</div> + + <div id="treeitem_haspopup_true" role="treeitem" aria-haspopup="true">This treeitem has aria-haspopup="true" and should get STATE_HASPOPUP.</div> + <div id="treeitem_haspopup_false" role="treeitem" aria-haspopup="false">This treeitem has aria-haspopup="false" and should get STATE_HASPOPUP.</div> + <div id="treeitem_haspopup_empty" role="treeitem" aria-haspopup="">This treeitem has aria-haspopup="" and should <emph>not</emph> get STATE_HASPOPUP.</div> + <div id="treeitem_haspopup_undefined" role="treeitem" aria-haspopup="undefined">This treeitem has aria-haspopup="undefined" and should <emph>not</emph> get STATE_HASPOPUP.</div> + <div id="treeitem_haspopup_absent" role="treeitem">This treeitem has <emph>no</emph> aria-haspopup attribute and should <emph>not</emph> get STATE_HASPOPUP.</div> + + <div id="treeitem_expanded_true" role="treeitem" aria-expanded="true">This treeitem has aria-expanded="true" and should get STATE_EXPANDABLE. It should also get STATE_EXPANDED.</div> + <div id="treeitem_expanded_false" role="treeitem" aria-expanded="false">This treeitem has aria-expanded="false" and should get STATE_EXPANDABLE. It should also get STATE_COLLAPSED.</div> + <div id="treeitem_expanded_empty" role="treeitem" aria-expanded="">This treeitem has aria-expanded="" and should <emph>not</emph> get STATE_EXPANDABLE.</div> + <div id="treeitem_expanded_undefined" role="treeitem" aria-expanded="undefined">This treeitem has aria-expanded="undefined" and should <emph>not</emph> get STATE_EXPANDABLE.</div> + <div id="treeitem_expanded_absent" role="treeitem">This treeitem has <emph>no</emph> aria-expanded attribute and should <emph>not</emph> get STATE_EXPANDABLE.</div> + </div> + + </body> +</html> diff --git a/accessible/tests/mochitest/test_bug420863.html b/accessible/tests/mochitest/test_bug420863.html new file mode 100644 index 0000000000..4f3a608fe1 --- /dev/null +++ b/accessible/tests/mochitest/test_bug420863.html @@ -0,0 +1,99 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=420863 +--> +<head> + <title>Table indexes chrome tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="common.js"></script> + <script type="application/javascript" + src="events.js"></script> + <script type="application/javascript" + src="actions.js"></script> + + <script type="application/javascript"> + var gClickHandler = null; + + function doTest() { + // Actions should be exposed on any accessible having related DOM node + // with registered 'click' event handler. + + // //////////////////////////////////////////////////////////////////////// + // generic td + var td1Acc = getAccessible("td1"); + if (!td1Acc) { + SimpleTest.finish(); + return; + } + + is(td1Acc.actionCount, 0, + "Simple table cell shouldn't have any actions"); + + // //////////////////////////////////////////////////////////////////////// + // one td with 'onclick' attribute and one with registered click handler + var td3Node = getNode("td3"); + + // register 'click' event handler + gClickHandler = { + handleEvent: function handleEvent(aEvent) { + }, + }; + td3Node.addEventListener("click", gClickHandler); + + // check actions + var actionsArray = [ + { + ID: "td2", // "onclick" attribute + actionName: "click", + actionIndex: 0, + events: CLICK_EVENTS, + }, + { + ID: td3Node, + actionName: "click", + actionIndex: 0, + events: CLICK_EVENTS, + checkOnClickEvent: function check(aEvent) { + // unregister click event handler + this.ID.removeEventListener("click", gClickHandler); + + // check actions + is(getAccessible(this.ID).actionCount, 0, + "td3 shouldn't have actions"); + }, + }, + ]; + + testActions(actionsArray); // will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a href="https://bugzilla.mozilla.org/show_bug.cgi?id=420863" + title="If an HTML element has an onClick attribute, expose its click action on the element rather than its child text leaf node." + target="_blank">Mozilla Bug 420863</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table> + <tr> + <td id="td1">Can't click this cell</td> + <td onclick="gTdClickAttr = true;" + id="td2">Cell with 'onclick' attribute</td> + <td id="td3">Cell with registered 'click' event handler</td> + </tr> + </table> + +</body> +</html> diff --git a/accessible/tests/mochitest/test_descr.html b/accessible/tests/mochitest/test_descr.html new file mode 100644 index 0000000000..c386ee5dc1 --- /dev/null +++ b/accessible/tests/mochitest/test_descr.html @@ -0,0 +1,134 @@ +<html> + +<head> + <title>nsIAccessible::description tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="common.js"></script> + <script type="application/javascript" + src="name.js"></script> + + <script type="application/javascript"> + function doTest() { + // Description from aria-describedby attribute + testDescr("img1", "aria description"); + + // No description from @title attribute because it is used to generate + // name. + testDescr("img2", ""); + + // Description from @title attribute, name is generated from @alt + // attribute. + testDescr("img3", "description"); + + // No description from aria-describedby since it is the same as the + // @alt attribute which is used as the name + testDescr("img4", ""); + + // No description from @title attribute since it is the same as the + // @alt attribute which is used as the name + testDescr("img5", ""); + + // Description from content of h2. + testDescr("p", "heading"); + + // Description from aria-description attribute + testDescr("p2", "I describe"); + + // Description from contents of h2 when both aria-describedby and + // aria-description are present + testDescr("p3", "heading"); + + // From table summary (caption is used as a name) + testDescr("table1", "summary"); + + // Empty (summary is used as a name) + testDescr("table2", ""); + + // From title (summary is used as a name) + testDescr("table3", "title"); + + // No description from <desc> element since it is the same as the + // <title> element. + testDescr("svg", ""); + + // role="alert" referenced by aria-describedby should include subtree. + testDescr("inputDescribedByAlert", "Error"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=489944" + title="@title attribute no longer exposed on accDescription"> + Mozilla Bug 489944 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=666212" + title="summary attribute content mapped to accessible name in MSAA"> + Mozilla Bug 666212 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi/id=1031188" + title="Ensure that accDescription never duplicates AccessibleName"> + Mozilla Bug 1031188 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="description">aria description</p> + <img id="img1" aria-describedby="description" /> + <img id="img2" title="title" /> + <img id="img3" alt="name" title="description" /> + <img id="img4" alt="aria description" aria-describedby="description"> + <img id="img5" alt="image" title="image"> + + <h2 id="heading">heading</h2> + <p id="p" aria-describedby="heading" role="button">click me</p> + <p id="p2" aria-description="I describe" role="button">click me</p> + <p id="p3" aria-description="I do not describe" aria-describedby="heading" role="button">click me</p> + + <table id="table1" summary="summary"> + <caption>caption</caption> + <tr><td>cell</td></tr> + </table> + + <table id="table2" summary="summary"> + <tr><td>cell</td></tr> + </table> + + <table id="table3" summary="summary" title="title"> + <tr><td>cell</td></tr> + </table> + + <svg xmlns="http://www.w3.org/2000/svg" version="1.1" + viewBox="0 0 100 100" preserveAspectRatio="xMidYMid slice" + id="svg" + style="width:100px; height:100px;"> + <title>SVG Image</title> + <desc>SVG Image</desc> + <linearGradient id="gradient"> + <stop class="begin" offset="0%"/> + <stop class="end" offset="100%"/> + </linearGradient> + <rect x="0" y="0" width="100" height="100" style="fill:url(#gradient)" /> + <circle cx="50" cy="50" r="30" style="fill:url(#gradient)" /> + </svg> + + <div id="alert" role="alert">Error</div> + <input type="text" id="inputDescribedByAlert" aria-describedby="alert"> +</body> +</html> diff --git a/accessible/tests/mochitest/test_nsIAccessibleDocument.html b/accessible/tests/mochitest/test_nsIAccessibleDocument.html new file mode 100644 index 0000000000..7758f63805 --- /dev/null +++ b/accessible/tests/mochitest/test_nsIAccessibleDocument.html @@ -0,0 +1,94 @@ +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=441737 +--> +<head> + <title>nsIAccessibleDocument chrome tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="common.js"></script> + <script type="application/javascript" + src="role.js"></script> + <script type="application/javascript" + src="states.js"></script> + + <script src="chrome://mochikit/content/chrome-harness.js"></script> + + <script type="application/javascript"> + function doTest() { + var docAcc = getAccessible(document, [nsIAccessibleDocument]); + if (docAcc) { + // nsIAccessible + is(docAcc.name, "nsIAccessibleDocument chrome tests", + "Name for document accessible not correct!"); + + testRole(docAcc, ROLE_DOCUMENT); + + // check if it is focusable, read-only. + testStates(docAcc, (STATE_READONLY | STATE_FOCUSABLE)); + + // No actions wanted on doc + is(docAcc.actionCount, 0, "Wrong number of actions for document!"); + + // attributes should contain tag:body + const attributes = docAcc.attributes; + is(attributes.getStringProperty("tag"), "body", + "Wrong attribute on document!"); + + // Document URL. + var rootDir = getRootDirectory(window.location.href); + is(docAcc.URL, rootDir + "test_nsIAccessibleDocument.html", + "Wrong URL for document!"); + + // Document title and mime type. + is(docAcc.title, "nsIAccessibleDocument chrome tests", + "Wrong title for document!"); + is(docAcc.mimeType, "text/html", + "Wrong mime type for document!"); + + // DocAccessible::getDocType currently returns NS_ERROR_FAILURE. + // See bug 442005. After fixing, please remove this comment and + // uncomment the below two lines to enable the test. +// is(docAcc.docType, "HTML", +// "Wrong type of document!"); + + // Test for correct Document retrieval. + var domDoc = null; + try { + domDoc = docAcc.DOMDocument; + } catch (e) {} + ok(domDoc, "no Document for this doc accessible!"); + is(domDoc, document, "Document nodes do not match!"); + + // Test for correct nsIDOMWindow retrieval. + var domWindow = null; + try { + domWindow = docAcc.window; + } catch (e) {} + ok(domWindow, "no nsIDOMWindow for this doc accessible!"); + is(domWindow, window, "Window nodes do not match!"); + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=441737" + title="nsAccessibleDocument chrome tests"> + Mozilla Bug 441737 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/test_nsIAccessibleImage.html b/accessible/tests/mochitest/test_nsIAccessibleImage.html new file mode 100644 index 0000000000..77bc74eceb --- /dev/null +++ b/accessible/tests/mochitest/test_nsIAccessibleImage.html @@ -0,0 +1,198 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=429659 +--> +<head> + <title>nsIAccessibleImage chrome tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="common.js"></script> + <script type="application/javascript" + src="role.js"></script> + <script type="application/javascript" + src="attributes.js"></script> + <script type="application/javascript" + src="layout.js"></script> + + <script type="application/javascript"> + function testCoordinates(aID, aAcc, aWidth, aHeight) { + var screenX = {}, screenY = {}, windowX = {}, windowY = {}, parentX = {}, + parentY = {}; + + // get screen coordinates. + aAcc.getImagePosition( + nsIAccessibleCoordinateType.COORDTYPE_SCREEN_RELATIVE, + screenX, screenY); + // get window coordinates. + aAcc.getImagePosition( + nsIAccessibleCoordinateType.COORDTYPE_WINDOW_RELATIVE, + windowX, windowY); + // get parent related coordinates. + aAcc.getImagePosition( + nsIAccessibleCoordinateType.COORDTYPE_PARENT_RELATIVE, + parentX, parentY); + // XXX For linked images, a negative parentY value is returned, and the + // screenY coordinate is the link's screenY coordinate minus 1. + // Until this is fixed, set parentY to -1 if it's negative. + if (parentY.value < 0) + parentY.value = -1; + + // See if asking image for child at image's screen coordinates gives + // correct accessible. getChildAtPoint operates on screen coordinates. + var tempAcc = null; + try { + tempAcc = aAcc.getChildAtPoint(screenX.value, screenY.value); + } catch (e) {} + is(tempAcc, aAcc, + "Wrong accessible returned for position of " + aID + "!"); + + // get image's parent. + var imageParentAcc = null; + try { + imageParentAcc = aAcc.parent; + } catch (e) {} + ok(imageParentAcc, "no parent accessible for " + aID + "!"); + + if (imageParentAcc) { + // See if parent's screen coordinates plus image's parent relative + // coordinates equal to image's screen coordinates. + var parentAccX = {}, parentAccY = {}, parentAccWidth = {}, + parentAccHeight = {}; + imageParentAcc.getBounds(parentAccX, parentAccY, parentAccWidth, + parentAccHeight); + is(parentAccX.value + parentX.value, screenX.value, + "Wrong screen x coordinate for " + aID + "!"); +// XXX see bug 456344 is(parentAccY.value + parentY.value, screenY.value, +// "Wrong screen y coordinate for " + aID + "!"); + } + + var [expected_w, expected_h] = CSSToDevicePixels(window, aWidth, aHeight); + var width = {}, height = {}; + aAcc.getImageSize(width, height); + is(width.value, expected_w, "Wrong width for " + aID + "!"); + is(height.value, expected_h, "wrong height for " + aID + "!"); + } + + function testThis(aID, aSRC, aWidth, aHeight, + aActionCount, aActionNames) { + var acc = getAccessible(aID, [nsIAccessibleImage]); + if (!acc) + return; + + // Test role + testRole(aID, ROLE_GRAPHIC); + + // test coordinates and size + testCoordinates(aID, acc, aWidth, aHeight); + + // bug 429659: Make sure the SRC attribute is set for any image + var attributes = {"src": aSRC}; + testAttrs(acc, attributes, true); + + var actionCount = aActionCount || 0; + is(acc.actionCount, actionCount, + "Wrong number of actions for " + aID + "!"); + if (actionCount) { + for (let index = 0; index < aActionNames.length; index++) { + is(acc.getActionName(index), aActionNames[index], + "Wrong action name for " + aID + ", index " + index + "!"); + } + } + } + + function doTest() { + // Test non-linked image + testThis("nonLinkedImage", "moz.png", 89, 38); + + // Test linked image + var actionNamesArray = ["jump"]; + testThis("linkedImage", "moz.png", 89, 38, 1, + actionNamesArray); + + // Image with long desc + actionNamesArray = ["showlongdesc"]; + testThis("longdesc", "moz.png", 89, 38, 1, + actionNamesArray); + + // Image with invalid url in long desc + testThis("invalidLongdesc", "moz.png", 89, 38, 0); + + // Image with click and long desc + actionNamesArray = null; + actionNamesArray = ["click", "showlongdesc"]; + testThis("clickAndLongdesc", "moz.png", + 89, 38, 2, actionNamesArray); + + // Image with click + actionNamesArray = null; + actionNamesArray = ["click"]; + testThis("click", "moz.png", + 89, 38, 1, actionNamesArray); + + // Image with long desc + actionNamesArray = null; + actionNamesArray = ["showlongdesc"]; + testThis("longdesc2", "moz.png", + 89, 38, 1, actionNamesArray); + + // Image described by HTML:a@href with whitespaces + actionNamesArray = null; + actionNamesArray = ["showlongdesc"]; + testThis("longdesc3", "moz.png", + 89, 38, 1, actionNamesArray); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=429659">Mozilla Bug 429659</a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=652635" + title="fall back missing @longdesc to aria-describedby pointing to a href"> + Mozilla Bug 652635 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <br>Simple image:<br> + <img id="nonLinkedImage" src="moz.png"/> + <br>Linked image:<br> + <a href="http://www.mozilla.org"><img id="linkedImage" src="moz.png"></a> + <br>Image with longdesc:<br> + <img id="longdesc" src="moz.png" longdesc="longdesc_src.html" + alt="Image of Mozilla logo"/> + <br>Image with invalid url in longdesc:<br> + <img id="invalidLongdesc" src="moz.png" longdesc="longdesc src.html" + alt="Image of Mozilla logo"/> + <br>Image with click and longdesc:<br> + <img id="clickAndLongdesc" src="moz.png" longdesc="longdesc_src.html" + alt="Another image of Mozilla logo" onclick="alert('Clicked!');"/> + + <br>image described by a link to be treated as longdesc<br> + <img id="longdesc2" src="moz.png" aria-describedby="describing_link" + alt="Second Image of Mozilla logo"/> + <a id="describing_link" href="longdesc_src.html">link to description of image</a> + + <br>Image described by a link to be treated as longdesc with whitespaces<br> + <img id="longdesc3" src="moz.png" aria-describedby="describing_link2" + alt="Second Image of Mozilla logo"/> + <a id="describing_link2" href="longdesc src.html">link to description of image</a> + + <br>Image with click:<br> + <img id="click" src="moz.png" + alt="A third image of Mozilla logo" onclick="alert('Clicked, too!');"/> + +</body> +</html> diff --git a/accessible/tests/mochitest/text.js b/accessible/tests/mochitest/text.js new file mode 100644 index 0000000000..21392336a1 --- /dev/null +++ b/accessible/tests/mochitest/text.js @@ -0,0 +1,814 @@ +/* import-globals-from common.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Public + +const BOUNDARY_CHAR = nsIAccessibleText.BOUNDARY_CHAR; +const BOUNDARY_WORD_START = nsIAccessibleText.BOUNDARY_WORD_START; +const BOUNDARY_WORD_END = nsIAccessibleText.BOUNDARY_WORD_END; +const BOUNDARY_LINE_START = nsIAccessibleText.BOUNDARY_LINE_START; +const BOUNDARY_LINE_END = nsIAccessibleText.BOUNDARY_LINE_END; +const BOUNDARY_PARAGRAPH = nsIAccessibleText.BOUNDARY_PARAGRAPH; + +const kTextEndOffset = nsIAccessibleText.TEXT_OFFSET_END_OF_TEXT; +const kCaretOffset = nsIAccessibleText.TEXT_OFFSET_CARET; + +const EndPoint_Start = nsIAccessibleTextRange.EndPoint_Start; +const EndPoint_End = nsIAccessibleTextRange.EndPoint_End; + +const kTodo = 1; // a test is expected to fail +const kOk = 2; // a test doesn't fail + +/** + * Test characterCount for the given array of accessibles. + * + * @param aCount [in] the expected character count + * @param aIDs [in] array of accessible identifiers to test + * @param aTodoFlag [in, optional] either kOk or kTodo + */ +function testCharacterCount(aIDs, aCount, aTodoFlag) { + var ids = aIDs instanceof Array ? aIDs : [aIDs]; + var isFunc = aTodoFlag == kTodo ? todo_is : is; + for (var i = 0; i < ids.length; i++) { + var textacc = getAccessible(ids[i], [nsIAccessibleText]); + isFunc( + textacc.characterCount, + aCount, + "Wrong character count for " + prettyName(ids[i]) + ); + } +} + +/** + * Test text between two given offsets. + * + * @param aIDs [in] an array of accessible IDs to test + * @param aStartOffset [in] the start offset within the text to test + * @param aEndOffset [in] the end offset up to which the text is tested + * @param aText [in] the expected result from the test + * @param aTodoFlag [in, optional] either kOk or kTodo + */ +function testText(aIDs, aStartOffset, aEndOffset, aText, aTodoFlag) { + var ids = aIDs instanceof Array ? aIDs : [aIDs]; + var isFunc = aTodoFlag == kTodo ? todo_is : is; + for (var i = 0; i < ids.length; i++) { + var acc = getAccessible(ids[i], nsIAccessibleText); + try { + isFunc( + acc.getText(aStartOffset, aEndOffset), + aText, + "getText: wrong text between start and end offsets '" + + aStartOffset + + "', '" + + aEndOffset + + " for '" + + prettyName(ids[i]) + + "'" + ); + } catch (e) { + ok( + false, + "getText fails between start and end offsets '" + + aStartOffset + + "', '" + + aEndOffset + + " for '" + + prettyName(ids[i]) + + "'" + ); + } + } +} + +/** + * Test getTextAtOffset for BOUNDARY_CHAR over different elements. + * + * @param aIDs [in] the accessible identifier or array of accessible + * identifiers + * @param aOffset [in] the offset to get a character at it + * @param aChar [in] the expected character + * @param aStartOffset [in] expected start offset of the character + * @param aEndOffset [in] expected end offset of the character + */ +function testCharAtOffset(aIDs, aOffset, aChar, aStartOffset, aEndOffset) { + var IDs = aIDs instanceof Array ? aIDs : [aIDs]; + for (var i = 0; i < IDs.length; i++) { + var acc = getAccessible(IDs[i], nsIAccessibleText); + testTextHelper( + IDs[i], + aOffset, + BOUNDARY_CHAR, + aChar, + aStartOffset, + aEndOffset, + kOk, + kOk, + kOk, + acc.getTextAtOffset, + "getTextAtOffset " + ); + } +} + +/** + * Test getTextAtOffset function over different elements. + * + * @param aIDs [in] ID or array of IDs + * @param aBoundaryType [in] boundary type for text to be retrieved + * @param aTestList [in] array of sets: + * offset1 and offset2 defining the offset range + * the text in the range + * start offset of the text in the range + * end offset of the text in the range + * + * or + * + * @param aOffset [in] the offset to get the text at + * @param aBoundaryType [in] Boundary type for text to be retrieved + * @param aText [in] expected return text for getTextAtOffset + * @param aStartOffset [in] expected return start offset for getTextAtOffset + * @param aEndOffset [in] expected return end offset for getTextAtOffset + * @param ... [in] list of ids or list of tuples made of: + * element identifier + * kTodo or kOk for returned text + * kTodo or kOk for returned start offset + * kTodo or kOk for returned offset result + */ +function testTextAtOffset() { + testTextSuperHelper("getTextAtOffset", arguments); +} + +/** + * Test getTextAfterOffset for BOUNDARY_CHAR over different elements. + * + * @param aIDs [in] the accessible identifier or array of accessible + * identifiers + * @param aOffset [in] the offset to get a character after it + * @param aChar [in] the expected character + * @param aStartOffset [in] expected start offset of the character + * @param aEndOffset [in] expected end offset of the character + */ +function testCharAfterOffset(aIDs, aOffset, aChar, aStartOffset, aEndOffset) { + var IDs = aIDs instanceof Array ? aIDs : [aIDs]; + for (var i = 0; i < IDs.length; i++) { + var acc = getAccessible(IDs[i], nsIAccessibleText); + testTextHelper( + IDs[i], + aOffset, + BOUNDARY_CHAR, + aChar, + aStartOffset, + aEndOffset, + kOk, + kOk, + kOk, + acc.getTextAfterOffset, + "getTextAfterOffset " + ); + } +} + +/** + * Test getTextAfterOffset function over different elements + * + * @param aIDs [in] ID or array of IDs + * @param aBoundaryType [in] boundary type for text to be retrieved + * @param aTestList [in] array of sets: + * offset1 and offset2 defining the offset range + * the text in the range + * start offset of the text in the range + * end offset of the text in the range + * + * or + * + * @param aOffset [in] the offset to get the text after + * @param aBoundaryType [in] Boundary type for text to be retrieved + * @param aText [in] expected return text for getTextAfterOffset + * @param aStartOffset [in] expected return start offset for getTextAfterOffset + * @param aEndOffset [in] expected return end offset for getTextAfterOffset + * @param ... [in] list of ids or list of tuples made of: + * element identifier + * kTodo or kOk for returned text + * kTodo or kOk for returned start offset + * kTodo or kOk for returned offset result + */ +function testTextAfterOffset( + aOffset, + aBoundaryType, + aText, + aStartOffset, + aEndOffset +) { + testTextSuperHelper("getTextAfterOffset", arguments); +} + +/** + * Test getTextBeforeOffset for BOUNDARY_CHAR over different elements. + * + * @param aIDs [in] the accessible identifier or array of accessible + * identifiers + * @param aOffset [in] the offset to get a character before it + * @param aChar [in] the expected character + * @param aStartOffset [in] expected start offset of the character + * @param aEndOffset [in] expected end offset of the character + */ +function testCharBeforeOffset(aIDs, aOffset, aChar, aStartOffset, aEndOffset) { + var IDs = aIDs instanceof Array ? aIDs : [aIDs]; + for (var i = 0; i < IDs.length; i++) { + var acc = getAccessible(IDs[i], nsIAccessibleText); + testTextHelper( + IDs[i], + aOffset, + BOUNDARY_CHAR, + aChar, + aStartOffset, + aEndOffset, + kOk, + kOk, + kOk, + acc.getTextBeforeOffset, + "getTextBeforeOffset " + ); + } +} + +/** + * Test getTextBeforeOffset function over different elements + * + * @param aIDs [in] ID or array of IDs + * @param aBoundaryType [in] boundary type for text to be retrieved + * @param aTestList [in] array of sets: + * offset1 and offset2 defining the offset range + * the text in the range + * start offset of the text in the range + * end offset of the text in the range + * + * or + * + * @param aOffset [in] the offset to get the text before + * @param aBoundaryType [in] Boundary type for text to be retrieved + * @param aText [in] expected return text for getTextBeforeOffset + * @param aStartOffset [in] expected return start offset for getTextBeforeOffset + * @param aEndOffset [in] expected return end offset for getTextBeforeOffset + * @param ... [in] list of ids or list of tuples made of: + * element identifier + * kTodo or kOk for returned text + * kTodo or kOk for returned start offset + * kTodo or kOk for returned offset result + */ +function testTextBeforeOffset( + aOffset, + aBoundaryType, + aText, + aStartOffset, + aEndOffset +) { + testTextSuperHelper("getTextBeforeOffset", arguments); +} + +/** + * Test word count for an element. + * + * @param aElement [in] element identifier + * @param aCount [in] Expected word count + * @param aToDoFlag [in] kTodo or kOk for returned text + */ +function testWordCount(aElement, aCount, aToDoFlag) { + var isFunc = aToDoFlag == kTodo ? todo_is : is; + var acc = getAccessible(aElement, nsIAccessibleText); + var startOffsetObj = {}, + endOffsetObj = {}; + var length = acc.characterCount; + var offset = 0; + var wordCount = 0; + while (true) { + acc.getTextAtOffset( + offset, + BOUNDARY_WORD_START, + startOffsetObj, + endOffsetObj + ); + if (offset >= length) { + break; + } + + wordCount++; + offset = endOffsetObj.value; + } + isFunc( + wordCount, + aCount, + "wrong words count for '" + + acc.getText(0, -1) + + "': " + + wordCount + + " in " + + prettyName(aElement) + ); +} + +/** + * Test word at a position for an element. + * + * @param aElement [in] element identifier + * @param aWordIndex [in] index of the word to test + * @param aText [in] expected text for that word + * @param aToDoFlag [in] kTodo or kOk for returned text + */ +function testWordAt(aElement, aWordIndex, aText, aToDoFlag) { + var isFunc = aToDoFlag == kTodo ? todo_is : is; + var acc = getAccessible(aElement, nsIAccessibleText); + + var textLength = acc.characterCount; + var wordIdx = aWordIndex; + var startOffsetObj = { value: 0 }, + endOffsetObj = { value: 0 }; + for (let offset = 0; offset < textLength; offset = endOffsetObj.value) { + acc.getTextAtOffset( + offset, + BOUNDARY_WORD_START, + startOffsetObj, + endOffsetObj + ); + + wordIdx--; + if (wordIdx < 0) { + break; + } + } + + if (wordIdx >= 0) { + ok( + false, + "the given word index '" + + aWordIndex + + "' exceeds words amount in " + + prettyName(aElement) + ); + + return; + } + + var startWordOffset = startOffsetObj.value; + var endWordOffset = endOffsetObj.value; + + // Calculate the end word offset. + acc.getTextAtOffset( + endOffsetObj.value, + BOUNDARY_WORD_END, + startOffsetObj, + endOffsetObj + ); + if (startOffsetObj.value != textLength) { + endWordOffset = startOffsetObj.value; + } + + if (endWordOffset <= startWordOffset) { + todo( + false, + "wrong start and end offset for word at index '" + + aWordIndex + + "': " + + " of text '" + + acc.getText(0, -1) + + "' in " + + prettyName(aElement) + ); + + return; + } + + let text = acc.getText(startWordOffset, endWordOffset); + isFunc( + text, + aText, + "wrong text for word at index '" + + aWordIndex + + "': " + + " of text '" + + acc.getText(0, -1) + + "' in " + + prettyName(aElement) + ); +} + +/** + * Test words in a element. + * + * @param aElement [in] element identifier + * @param aWords [in] array of expected words + * @param aToDoFlag [in, optional] kTodo or kOk for returned text + */ +function testWords(aElement, aWords, aToDoFlag) { + if (aToDoFlag == null) { + aToDoFlag = kOk; + } + + testWordCount(aElement, aWords.length, aToDoFlag); + + for (var i = 0; i < aWords.length; i++) { + testWordAt(aElement, i, aWords[i], aToDoFlag); + } +} + +/** + * Remove all selections. + * + * @param aID [in] Id, DOM node, or acc obj + */ +function cleanTextSelections(aID) { + var acc = getAccessible(aID, [nsIAccessibleText]); + + while (acc.selectionCount > 0) { + acc.removeSelection(0); + } +} + +/** + * Test addSelection method. + * + * @param aID [in] Id, DOM node, or acc obj + * @param aStartOffset [in] start offset for the new selection + * @param aEndOffset [in] end offset for the new selection + * @param aSelectionsCount [in] expected number of selections after addSelection + */ +function testTextAddSelection(aID, aStartOffset, aEndOffset, aSelectionsCount) { + var acc = getAccessible(aID, [nsIAccessibleText]); + var text = acc.getText(0, -1); + + acc.addSelection(aStartOffset, aEndOffset); + + is( + acc.selectionCount, + aSelectionsCount, + text + + ": failed to add selection from offset '" + + aStartOffset + + "' to offset '" + + aEndOffset + + "': selectionCount after" + ); +} + +/** + * Test removeSelection method. + * + * @param aID [in] Id, DOM node, or acc obj + * @param aSelectionIndex [in] index of the selection to be removed + * @param aSelectionsCount [in] expected number of selections after + * removeSelection + */ +function testTextRemoveSelection(aID, aSelectionIndex, aSelectionsCount) { + var acc = getAccessible(aID, [nsIAccessibleText]); + var text = acc.getText(0, -1); + + acc.removeSelection(aSelectionIndex); + + is( + acc.selectionCount, + aSelectionsCount, + text + + ": failed to remove selection at index '" + + aSelectionIndex + + "': selectionCount after" + ); +} + +/** + * Test setSelectionBounds method. + * + * @param aID [in] Id, DOM node, or acc obj + * @param aStartOffset [in] new start offset for the selection + * @param aEndOffset [in] new end offset for the selection + * @param aSelectionIndex [in] index of the selection to set + * @param aSelectionsCount [in] expected number of selections after + * setSelectionBounds + */ +function testTextSetSelection( + aID, + aStartOffset, + aEndOffset, + aSelectionIndex, + aSelectionsCount +) { + var acc = getAccessible(aID, [nsIAccessibleText]); + var text = acc.getText(0, -1); + + acc.setSelectionBounds(aSelectionIndex, aStartOffset, aEndOffset); + + is( + acc.selectionCount, + aSelectionsCount, + text + + ": failed to set selection at index '" + + aSelectionIndex + + "': selectionCount after" + ); +} + +/** + * Test selectionCount method. + * + * @param aID [in] Id, DOM node, or acc obj + * @param aCount [in] expected selection count + */ +function testTextSelectionCount(aID, aCount) { + var acc = getAccessible(aID, [nsIAccessibleText]); + var text = acc.getText(0, -1); + + is(acc.selectionCount, aCount, text + ": wrong selectionCount: "); +} + +/** + * Test getSelectionBounds method. + * + * @param aID [in] Id, DOM node, or acc obj + * @param aStartOffset [in] expected start offset for the selection + * @param aEndOffset [in] expected end offset for the selection + * @param aSelectionIndex [in] index of the selection to get + */ +function testTextGetSelection(aID, aStartOffset, aEndOffset, aSelectionIndex) { + var acc = getAccessible(aID, [nsIAccessibleText]); + var text = acc.getText(0, -1); + + var startObj = {}, + endObj = {}; + acc.getSelectionBounds(aSelectionIndex, startObj, endObj); + + is( + startObj.value, + aStartOffset, + text + ": wrong start offset for index '" + aSelectionIndex + "'" + ); + is( + endObj.value, + aEndOffset, + text + ": wrong end offset for index '" + aSelectionIndex + "'" + ); +} + +function testTextRange( + aRange, + aRangeDescr, + aStartContainer, + aStartOffset, + aEndContainer, + aEndOffset, + aText, + aCommonContainer, + aChildren +) { + isObject( + aRange.startContainer, + getAccessible(aStartContainer), + "Wrong start container of " + aRangeDescr + ); + is(aRange.startOffset, aStartOffset, "Wrong start offset of " + aRangeDescr); + isObject( + aRange.endContainer, + getAccessible(aEndContainer), + "Wrong end container of " + aRangeDescr + ); + is(aRange.endOffset, aEndOffset, "Wrong end offset of " + aRangeDescr); + + if (aText === undefined) { + return; + } + + is(aRange.text, aText, "Wrong text of " + aRangeDescr); + + var children = aRange.embeddedChildren; + is( + children ? children.length : 0, + aChildren ? aChildren.length : 0, + "Wrong embedded children count of " + aRangeDescr + ); + + isObject( + aRange.container, + getAccessible(aCommonContainer), + "Wrong container of " + aRangeDescr + ); + + if (aChildren) { + for (var i = 0; i < aChildren.length; i++) { + var expectedChild = getAccessible(aChildren[i]); + var actualChild = children.queryElementAt(i, nsIAccessible); + isObject( + actualChild, + expectedChild, + "Wrong child at index '" + i + "' of " + aRangeDescr + ); + } + } +} + +// ////////////////////////////////////////////////////////////////////////////// +// Private + +function testTextSuperHelper(aFuncName, aArgs) { + // List of tests. + if (aArgs[2] instanceof Array) { + let ids = aArgs[0] instanceof Array ? aArgs[0] : [aArgs[0]]; + let boundaryType = aArgs[1]; + let list = aArgs[2]; + for (let i = 0; i < list.length; i++) { + let offset1 = list[i][0], + offset2 = list[i][1]; + let text = list[i][2], + startOffset = list[i][3], + endOffset = list[i][4]; + let failureList = list[i][5]; + for (let offset = offset1; offset <= offset2; offset++) { + for (let idIdx = 0; idIdx < ids.length; idIdx++) { + let id = ids[idIdx]; + + let flagOk1 = kOk, + flagOk2 = kOk, + flagOk3 = kOk; + if (failureList) { + for (let fIdx = 0; fIdx < failureList.length; fIdx++) { + if ( + offset == failureList[fIdx][0] && + id == failureList[fIdx][1] + ) { + flagOk1 = failureList[fIdx][2]; + flagOk2 = failureList[fIdx][3]; + flagOk3 = failureList[fIdx][4]; + break; + } + } + } + + let acc = getAccessible(id, nsIAccessibleText); + testTextHelper( + id, + offset, + boundaryType, + text, + startOffset, + endOffset, + flagOk1, + flagOk2, + flagOk3, + acc[aFuncName], + aFuncName + " " + ); + } + } + } + return; + } + + // Test at single offset. List of IDs. + var offset = aArgs[0]; + var boundaryType = aArgs[1]; + var text = aArgs[2]; + var startOffset = aArgs[3]; + var endOffset = aArgs[4]; + if (aArgs[5] instanceof Array) { + let ids = aArgs[5]; + for (let i = 0; i < ids.length; i++) { + let acc = getAccessible(ids[i], nsIAccessibleText); + testTextHelper( + ids[i], + offset, + boundaryType, + text, + startOffset, + endOffset, + kOk, + kOk, + kOk, + acc[aFuncName], + aFuncName + " " + ); + } + + return; + } + + // Each ID is tested separately. + for (let i = 5; i < aArgs.length; i = i + 4) { + let ID = aArgs[i]; + let acc = getAccessible(ID, nsIAccessibleText); + let toDoFlag1 = aArgs[i + 1]; + let toDoFlag2 = aArgs[i + 2]; + let toDoFlag3 = aArgs[i + 3]; + + testTextHelper( + ID, + offset, + boundaryType, + text, + startOffset, + endOffset, + toDoFlag1, + toDoFlag2, + toDoFlag3, + acc[aFuncName], + aFuncName + " " + ); + } +} + +function testTextHelper( + aID, + aOffset, + aBoundaryType, + aText, + aStartOffset, + aEndOffset, + aToDoFlag1, + aToDoFlag2, + aToDoFlag3, + aTextFunc, + aTextFuncName +) { + var exceptionFlag = + aToDoFlag1 == undefined || + aToDoFlag2 == undefined || + aToDoFlag3 == undefined; + + var startMsg = aTextFuncName + "(" + boundaryToString(aBoundaryType) + "): "; + var endMsg = ", id: " + prettyName(aID) + ";"; + + try { + var startOffsetObj = {}, + endOffsetObj = {}; + var text = aTextFunc(aOffset, aBoundaryType, startOffsetObj, endOffsetObj); + + if (exceptionFlag) { + ok(false, startMsg + "no expected failure at offset " + aOffset + endMsg); + return; + } + + var isFunc1 = aToDoFlag1 == kTodo ? todo : ok; + var isFunc2 = aToDoFlag2 == kTodo ? todo : ok; + var isFunc3 = aToDoFlag3 == kTodo ? todo : ok; + + isFunc1( + text == aText, + startMsg + + "wrong text " + + "(got '" + + text + + "', expected: '" + + aText + + "')" + + ", offset: " + + aOffset + + endMsg + ); + isFunc2( + startOffsetObj.value == aStartOffset, + startMsg + + "wrong start offset" + + "(got '" + + startOffsetObj.value + + "', expected: '" + + aStartOffset + + "')" + + ", offset: " + + aOffset + + endMsg + ); + isFunc3( + endOffsetObj.value == aEndOffset, + startMsg + + "wrong end offset" + + "(got '" + + endOffsetObj.value + + "', expected: '" + + aEndOffset + + "')" + + ", offset: " + + aOffset + + endMsg + ); + } catch (e) { + var okFunc = exceptionFlag ? todo : ok; + okFunc( + false, + startMsg + "failed at offset " + aOffset + endMsg + ", exception: " + e + ); + } +} + +function boundaryToString(aBoundaryType) { + switch (aBoundaryType) { + case BOUNDARY_CHAR: + return "char"; + case BOUNDARY_WORD_START: + return "word start"; + case BOUNDARY_WORD_END: + return "word end"; + case BOUNDARY_LINE_START: + return "line start"; + case BOUNDARY_LINE_END: + return "line end"; + case BOUNDARY_PARAGRAPH: + return "paragraph"; + } + return "unknown"; +} diff --git a/accessible/tests/mochitest/text/a11y.ini b/accessible/tests/mochitest/text/a11y.ini new file mode 100644 index 0000000000..18c4982aa6 --- /dev/null +++ b/accessible/tests/mochitest/text/a11y.ini @@ -0,0 +1,19 @@ +[DEFAULT] +support-files = doc.html + !/accessible/tests/mochitest/*.js + +[test_atcaretoffset.html] +[test_charboundary.html] +[test_doc.html] +[test_dynamic.html] +[test_general.xhtml] +[test_gettext.html] +[test_hypertext.html] +[test_lineboundary.html] +[test_paragraphboundary.html] +[test_passwords.html] +[test_selection.html] +[test_settext_input_event.html] +[test_textBounds.html] +[test_wordboundary.html] +[test_words.html] diff --git a/accessible/tests/mochitest/text/doc.html b/accessible/tests/mochitest/text/doc.html new file mode 100644 index 0000000000..d57406c226 --- /dev/null +++ b/accessible/tests/mochitest/text/doc.html @@ -0,0 +1,9 @@ +<!DOCTYPE html> +<html> +<head> + <script type="application/javascript"> + document.documentElement.appendChild(document.createTextNode("outbody")); + </script> +</head> +<body>inbody</body> +</html> diff --git a/accessible/tests/mochitest/text/test_atcaretoffset.html b/accessible/tests/mochitest/text/test_atcaretoffset.html new file mode 100644 index 0000000000..486e615b65 --- /dev/null +++ b/accessible/tests/mochitest/text/test_atcaretoffset.html @@ -0,0 +1,423 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test: nsIAccessibleText getText* functions at caret offset</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../text.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; // debugging + + function traverseTextByLines(aQueue, aID, aLines) { + var wholeText = ""; + for (var i = 0; i < aLines.length ; i++) + wholeText += aLines[i][0] + aLines[i][1]; + + var baseInvokerFunc = synthClick; + var charIter = new charIterator(wholeText, aLines); + // charIter.debugOffset = 10; // enable to run tests at given offset only + + while (charIter.next()) { + aQueue.push(new tmpl_moveTo(aID, baseInvokerFunc, wholeText, charIter)); + baseInvokerFunc = synthRightKey; + } + } + + /** + * Used to get test list for each traversed character. + */ + function charIterator(aWholeText, aLines) { + this.next = function charIterator_next() { + // Don't increment offset if we are at end of the wrapped line + // (offset is shared between end of this line and start of next line). + if (this.mAtWrappedLineEnd) { + this.mAtWrappedLineEnd = false; + this.mLine = this.mLine.nextLine; + return true; + } + + this.mOffset++; + if (this.mOffset > aWholeText.length) + return false; + + var nextLine = this.mLine.nextLine; + if (!nextLine.isFakeLine() && this.mOffset == nextLine.start) { + if (nextLine.start == this.mLine.end) + this.mAtWrappedLineEnd = true; + else + this.mLine = nextLine; + } + + return true; + }; + + Object.defineProperty(this, "offset", { get() { return this.mOffset; }, + }); + + Object.defineProperty(this, "offsetDescr", { get() { + return this.mOffset + " offset (" + this.mLine.number + " line, " + + (this.mOffset - this.mLine.start) + " offset on the line)"; + }, + }); + + Object.defineProperty(this, "tests", { get() { + // Line boundary tests. + var cLine = this.mLine; + var pLine = cLine.prevLine; + var ppLine = pLine.prevLine; + var nLine = cLine.nextLine; + var nnLine = nLine.nextLine; + + var lineTests = [ + [ testTextBeforeOffset, BOUNDARY_LINE_START, pLine.start, cLine.start], + [ testTextBeforeOffset, BOUNDARY_LINE_END, ppLine.end, pLine.end], + [ testTextAtOffset, BOUNDARY_LINE_START, cLine.start, nLine.start], + [ testTextAtOffset, BOUNDARY_LINE_END, pLine.end, cLine.end], + [ testTextAfterOffset, BOUNDARY_LINE_START, nLine.start, nnLine.start], + [ testTextAfterOffset, BOUNDARY_LINE_END, cLine.end, nLine.end], + ]; + + // Word boundary tests. + var cWord = this.mLine.firstWord; + var nWord = cWord.nextWord, pWord = cWord.prevWord; + + // The current word is a farthest word starting at or after the offset. + if (this.mOffset >= nWord.start) { + while (this.mOffset >= nWord.start && !this.mLine.isLastWord(cWord)) { + cWord = nWord; + nWord = nWord.nextWord; + } + pWord = cWord.prevWord; + } else if (this.mOffset < cWord.start) { + while (this.mOffset < cWord.start) { + cWord = pWord; + pWord = pWord.prevWord; + } + nWord = cWord.nextWord; + } + + var nnWord = nWord.nextWord, ppWord = pWord.prevWord; + + var isAfterWordEnd = + this.mOffset > cWord.end || cWord.line != this.mLine; + var isAtOrAfterWordEnd = (this.mOffset >= cWord.end); + var useNextWordForAtWordEnd = + isAtOrAfterWordEnd && this.mOffset != aWholeText.length; + + var wordTests = [ + [ testTextBeforeOffset, BOUNDARY_WORD_START, + pWord.start, cWord.start ], + [ testTextBeforeOffset, BOUNDARY_WORD_END, + (isAfterWordEnd ? pWord : ppWord).end, + (isAfterWordEnd ? cWord : pWord).end ], + [ testTextAtOffset, BOUNDARY_WORD_START, + cWord.start, nWord.start ], + [ testTextAtOffset, BOUNDARY_WORD_END, + (useNextWordForAtWordEnd ? cWord : pWord).end, + (useNextWordForAtWordEnd ? nWord : cWord).end ], + [ testTextAfterOffset, BOUNDARY_WORD_START, + nWord.start, nnWord.start ], + [ testTextAfterOffset, BOUNDARY_WORD_END, + (isAfterWordEnd ? nWord : cWord).end, + (isAfterWordEnd ? nnWord : nWord).end ], + ]; + + // Character boundary tests. + var prevOffset = this.offset > 1 ? this.offset - 1 : 0; + var nextOffset = this.offset >= aWholeText.length ? + this.offset : this.offset + 1; + var nextAfterNextOffset = nextOffset >= aWholeText.length ? + nextOffset : nextOffset + 1; + + var charTests = [ + [ testTextBeforeOffset, BOUNDARY_CHAR, + prevOffset, this.offset ], + [ testTextAtOffset, BOUNDARY_CHAR, + this.offset, + this.mAtWrappedLineEnd ? this.offset : nextOffset ], + [ testTextAfterOffset, BOUNDARY_CHAR, + this.mAtWrappedLineEnd ? this.offset : nextOffset, + this.mAtWrappedLineEnd ? nextOffset : nextAfterNextOffset ], + ]; + + return lineTests.concat(wordTests.concat(charTests)); + }, + }); + + Object.defineProperty(this, "failures", { get() { + if (this.mOffset == this.mLine.start) + return this.mLine.lineStartFailures; + if (this.mOffset == this.mLine.end) + return this.mLine.lineEndFailures; + return []; + }, + }); + + this.mOffset = -1; + this.mLine = new line(aWholeText, aLines, 0); + this.mAtWrappedLineEnd = false; + this.mWord = this.mLine.firstWord; + } + + /** + * A line object. Allows to navigate by lines and by words. + */ + function line(aWholeText, aLines, aIndex) { + Object.defineProperty(this, "prevLine", { get() { + return new line(aWholeText, aLines, aIndex - 1); + }, + }); + Object.defineProperty(this, "nextLine", { get() { + return new line(aWholeText, aLines, aIndex + 1); + }, + }); + + Object.defineProperty(this, "start", { get() { + if (aIndex < 0) + return 0; + + if (aIndex >= aLines.length) + return aWholeText.length; + + return aLines[aIndex][2]; + }, + }); + Object.defineProperty(this, "end", { get() { + if (aIndex < 0) + return 0; + + if (aIndex >= aLines.length) + return aWholeText.length; + + return aLines[aIndex][3]; + }, + }); + + Object.defineProperty(this, "number", { get() { return aIndex; }, + }); + Object.defineProperty(this, "wholeText", { get() { return aWholeText; }, + }); + this.isFakeLine = function line_isFakeLine() { + return aIndex < 0 || aIndex >= aLines.length; + }; + + Object.defineProperty(this, "lastWord", { get() { + if (aIndex < 0) + return new word(this, [], -1); + if (aIndex >= aLines.length) + return new word(this, [], 0); + + var words = aLines[aIndex][4].words; + return new word(this, words, words.length - 2); + }, + }); + Object.defineProperty(this, "firstWord", { get() { + if (aIndex < 0) + return new word(this, [], -1); + if (aIndex >= aLines.length) + return new word(this, [], 0); + + var words = aLines[aIndex][4].words; + return new word(this, words, 0); + }, + }); + + this.isLastWord = function line_isLastWord(aWord) { + var lastWord = this.lastWord; + return lastWord.start == aWord.start && lastWord.end == aWord.end; + }; + + Object.defineProperty(this, "lineStartFailures", { get() { + if (aIndex < 0 || aIndex >= aLines.length) + return []; + + return aLines[aIndex][4].lsf || []; + }, + }); + Object.defineProperty(this, "lineEndFailures", { get() { + if (aIndex < 0 || aIndex >= aLines.length) + return []; + + return aLines[aIndex][4].lef || []; + }, + }); + } + + /** + * A word object. Allows to navigate by words. + */ + function word(aLine, aWords, aIndex) { + Object.defineProperty(this, "prevWord", { get() { + if (aIndex >= 2) + return new word(aLine, aWords, aIndex - 2); + + var prevLineLastWord = aLine.prevLine.lastWord; + if (this.start == prevLineLastWord.start && !this.isFakeStartWord()) + return prevLineLastWord.prevWord; + return prevLineLastWord; + }, + }); + Object.defineProperty(this, "nextWord", { get() { + if (aIndex + 2 < aWords.length) + return new word(aLine, aWords, aIndex + 2); + + var nextLineFirstWord = aLine.nextLine.firstWord; + if (this.end == nextLineFirstWord.end && !this.isFakeEndWord()) + return nextLineFirstWord.nextWord; + return nextLineFirstWord; + }, + }); + + Object.defineProperty(this, "line", { get() { return aLine; } }); + + Object.defineProperty(this, "start", { get() { + if (this.isFakeStartWord()) + return 0; + + if (this.isFakeEndWord()) + return aLine.end; + return aWords[aIndex]; + }, + }); + Object.defineProperty(this, "end", { get() { + if (this.isFakeStartWord()) + return 0; + + return this.isFakeEndWord() ? aLine.end : aWords[aIndex + 1]; + }, + }); + + this.toString = function word_toString() { + var start = this.start, end = this.end; + return "'" + aLine.wholeText.substring(start, end) + + "' at [" + start + ", " + end + "]"; + }; + + this.isFakeStartWord = function() { return aIndex < 0; }; + this.isFakeEndWord = function() { return aIndex >= aWords.length; }; + } + + /** + * A template invoker to move through the text. + */ + function tmpl_moveTo(aID, aInvokerFunc, aWholeText, aCharIter) { + this.offset = aCharIter.offset; + + var checker = new caretMoveChecker(this.offset, true, aID); + this.__proto__ = new (aInvokerFunc)(aID, checker); + + this.finalCheck = function genericMoveTo_finalCheck() { + if (this.noTests()) + return; + + for (var i = 0; i < this.tests.length; i++) { + var func = this.tests[i][0]; + var boundary = this.tests[i][1]; + var startOffset = this.tests[i][2]; + var endOffset = this.tests[i][3]; + var text = aWholeText.substring(startOffset, endOffset); + + var isOk1 = kOk, isOk2 = kOk, isOk3 = kOk; + for (var fIdx = 0; fIdx < this.failures.length; fIdx++) { + var failure = this.failures[fIdx]; + if (func.name.includes(failure[0]) && boundary == failure[1]) { + isOk1 = failure[2]; + isOk2 = failure[3]; + isOk3 = failure[4]; + } + } + + func(kCaretOffset, boundary, text, startOffset, endOffset, + aID, isOk1, isOk2, isOk3); + } + }; + + this.getID = function genericMoveTo_getID() { + return "move to " + this.offsetDescr; + }; + + this.noTests = function tmpl_moveTo_noTests() { + return ("debugOffset" in aCharIter) && + (aCharIter.debugOffset != this.offset); + }; + + this.offsetDescr = aCharIter.offsetDescr; + this.tests = this.noTests() ? null : aCharIter.tests; + this.failures = aCharIter.failures; + } + + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + + // __a__w__o__r__d__\n + // 0 1 2 3 4 5 + // __t__w__o__ (soft line break) + // 6 7 8 9 + // __w__o__r__d__s + // 10 11 12 13 14 15 + + traverseTextByLines(gQueue, "textarea", + [ [ "aword", "\n", 0, 5, { words: [ 0, 5 ] } ], + [ "two ", "", 6, 10, { words: [ 6, 9 ] } ], + [ "words", "", 10, 15, { words: [ 10, 15 ] } ], + ] ); + + var line4 = [ // "riend " + [ "TextBeforeOffset", BOUNDARY_WORD_END, kTodo, kTodo, kTodo ], + [ "TextAfterOffset", BOUNDARY_WORD_END, kTodo, kTodo, kTodo ], + ]; + traverseTextByLines(gQueue, "ta_wrapped", + [ [ "hi ", "", 0, 3, { words: [ 0, 2 ] } ], + [ "hello ", "", 3, 9, { words: [ 3, 8 ] } ], + [ "my ", "", 9, 12, { words: [ 9, 11 ] } ], + [ "longf", "", 12, 17, { words: [ 12, 17 ] } ], + [ "riend ", "", 17, 23, { words: [ 17, 22 ], lsf: line4 } ], + [ "t sq ", "", 23, 28, { words: [ 23, 24, 25, 27 ] } ], + [ "t", "", 28, 29, { words: [ 28, 29 ] } ], + ] ); + + gQueue.invoke(); // will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="nsIAccessibleText getText related functions tests at caret offset" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=852021"> + Bug 852021 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + + <textarea id="textarea" cols="5">aword +two words</textarea> + + <!-- scrollbar-width: none is needed so that the width of the scrollbar + doesn't incorrectly affect the width of the textarea on some systems. + See bug 1600170 and bug 33654. + --> + <textarea id="ta_wrapped" cols="5" style="scrollbar-width: none;">hi hello my longfriend t sq t</textarea> + </pre> +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_charboundary.html b/accessible/tests/mochitest/text/test_charboundary.html new file mode 100644 index 0000000000..5ca4120a47 --- /dev/null +++ b/accessible/tests/mochitest/text/test_charboundary.html @@ -0,0 +1,138 @@ +<!DOCTYPE html> +<html> +<head> + <title>Char boundary text tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // + // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + + var IDs = [ "i1", "d1", "e1", "t1" ]; + + testCharBeforeOffset(IDs, 0, "", 0, 0); + testCharBeforeOffset(IDs, 1, "h", 0, 1); + testCharBeforeOffset(IDs, 14, "n", 13, 14); + testCharBeforeOffset(IDs, 15, "d", 14, 15); + + testCharAtOffset(IDs, 0, "h", 0, 1); + testCharAtOffset(IDs, 1, "e", 1, 2); + testCharAtOffset(IDs, 14, "d", 14, 15); + testCharAtOffset(IDs, 15, "", 15, 15); + + testCharAfterOffset(IDs, 0, "e", 1, 2); + testCharAfterOffset(IDs, 1, "l", 2, 3); + testCharAfterOffset(IDs, 14, "", 15, 15); + testCharAfterOffset(IDs, 15, "", 15, 15); + + // //////////////////////////////////////////////////////////////////////// + // + // __B__r__a__v__e__ __S__i__r__ __ __R__o__b__i__n__ __ __ __r__a__n + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + + IDs = [ "i2", "d2", "e2", "t2" ]; + + testCharBeforeOffset(IDs, 0, "", 0, 0); + testCharBeforeOffset(IDs, 1, "B", 0, 1); + testCharBeforeOffset(IDs, 6, " ", 5, 6); + testCharBeforeOffset(IDs, 10, " ", 9, 10); + testCharBeforeOffset(IDs, 11, " ", 10, 11); + testCharBeforeOffset(IDs, 17, " ", 16, 17); + testCharBeforeOffset(IDs, 19, " ", 18, 19); + + testCharAtOffset(IDs, 0, "B", 0, 1); + testCharAtOffset(IDs, 1, "r", 1, 2); + testCharAtOffset(IDs, 5, " ", 5, 6); + testCharAtOffset(IDs, 9, " ", 9, 10); + testCharAtOffset(IDs, 10, " ", 10, 11); + testCharAtOffset(IDs, 17, " ", 17, 18); + + testCharAfterOffset(IDs, 0, "r", 1, 2); + testCharAfterOffset(IDs, 1, "a", 2, 3); + testCharAfterOffset(IDs, 4, " ", 5, 6); + testCharAfterOffset(IDs, 5, "S", 6, 7); + testCharAfterOffset(IDs, 8, " ", 9, 10); + testCharAfterOffset(IDs, 9, " ", 10, 11); + testCharAfterOffset(IDs, 10, "R", 11, 12); + testCharAfterOffset(IDs, 15, " ", 16, 17); + testCharAfterOffset(IDs, 16, " ", 17, 18); + testCharAfterOffset(IDs, 17, " ", 18, 19); + testCharAfterOffset(IDs, 18, "r", 19, 20); + + // //////////////////////////////////////////////////////////////////////// + // + // __o__n__e__w__o__r__d__\n + // 0 1 2 3 4 5 6 7 + // __\n + // 8 + // __t__w__o__ __w__o__r__d__s__\n + // 9 10 11 12 13 14 15 16 17 18 + + IDs = ["d3", "dbr3", "e3", "ebr3", "t3"]; + + testCharBeforeOffset(IDs, 8, "\n", 7, 8); + testCharBeforeOffset(IDs, 9, "\n", 8, 9); + testCharBeforeOffset(IDs, 10, "t", 9, 10); + + testCharAtOffset(IDs, 7, "\n", 7, 8); + testCharAtOffset(IDs, 8, "\n", 8, 9); + testCharAtOffset(IDs, 9, "t", 9, 10); + + testCharAfterOffset(IDs, 6, "\n", 7, 8); + testCharAfterOffset(IDs, 7, "\n", 8, 9); + testCharAfterOffset(IDs, 8, "t", 9, 10); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="i1" value="hello my friend"/> + <div id="d1">hello my friend</div> + <div id="e1" contenteditable="true">hello my friend</div> + <textarea id="t1" contenteditable="true">hello my friend</textarea> + + <input id="i2" value="Brave Sir Robin ran"/> + <pre> + <div id="d2">Brave Sir Robin ran</div> + <div id="e2" contenteditable="true">Brave Sir Robin ran</div> + </pre> + <textarea id="t2" cols="300">Brave Sir Robin ran</textarea> + + <pre> + <div id="d3">oneword + +two words +</div> + <div id="dbr3">oneword<br/><br/>two words<br/></div> + <div id="e3" contenteditable="true">oneword + +two words +</div> + <div id="ebr3" contenteditable="true">oneword<br/><br/>two words<br/></div> + <textarea id="t3" cols="300">oneword + +two words</textarea> + </pre> + +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_doc.html b/accessible/tests/mochitest/text/test_doc.html new file mode 100644 index 0000000000..88b75b98c4 --- /dev/null +++ b/accessible/tests/mochitest/text/test_doc.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessibleText getText related function tests for document accessible</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript"> + + function doTest() { + var iframeDoc = [ getNode("iframe").contentDocument ]; + testCharacterCount(iframeDoc, 15); + testText(iframeDoc, 0, 15, "outbody inbody "); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Elements appended outside the body aren't accessible" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=608887">Mozilla Bug 608887</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <iframe id="iframe" src="doc.html"></iframe> + +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_dynamic.html b/accessible/tests/mochitest/text/test_dynamic.html new file mode 100644 index 0000000000..63889fc664 --- /dev/null +++ b/accessible/tests/mochitest/text/test_dynamic.html @@ -0,0 +1,80 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessibleText getText related function tests for tree mutations</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function insertBefore(aId, aEl, aTextBefore, aTextAfter, aStartIdx, aEndIdx) { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, aId), + ]; + + this.invoke = function insertBefore_invoke() { + testText(aId, 0, -1, aTextBefore); + getNode(aId).insertBefore(aEl, getNode(aId).firstChild); + }; + + this.finalCheck = function insertBefore_finalCheck() { + testText(aId, aStartIdx, aEndIdx, aTextAfter); + }; + + this.getID = function insertTextBefore_getID() { + return "insert " + prettyName(aEl) + " before"; + }; + } + + function insertTextBefore(aId, aTextBefore, aText) { + var el = document.createTextNode(aText); + this.__proto__ = new insertBefore(aId, el, aTextBefore, + aText + aTextBefore, 0, -1); + } + + function insertImgBefore(aId, aTextBefore) { + var el = document.createElement("img"); + el.setAttribute("src", "../moz.png"); + el.setAttribute("alt", "mozilla"); + + this.__proto__ = new insertBefore(aId, el, aTextBefore, + kEmbedChar + aTextBefore, 0, -1); + } + + function insertTextBefore2(aId) { + var el = document.createTextNode("hehe"); + this.__proto__ = new insertBefore(aId, el, "ho", "ho", 4, -1); + } + + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + gQueue.push(new insertTextBefore("c1", "ho", "ha")); + gQueue.push(new insertImgBefore("c1", "haho")); + gQueue.push(new insertImgBefore("c2", kEmbedChar)); + gQueue.push(new insertTextBefore2("c3")); + gQueue.invoke(); // will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="c1">ho</div> + <div id="c2"><img src="../moz.png" alt="mozilla"></div> + <div id="c3">ho</div> +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_general.xhtml b/accessible/tests/mochitest/text/test_general.xhtml new file mode 100644 index 0000000000..df0ffcc0c6 --- /dev/null +++ b/accessible/tests/mochitest/text/test_general.xhtml @@ -0,0 +1,79 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Tests: XUL label text interface"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../text.js"></script> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Testing + + var gQueue = null; + function doTests() + { + ////////////////////////////////////////////////////////////////////////// + // XUL label + + var ids = ["label1", "label2"]; + + testCharacterCount(ids, 5); + + testText(ids, 0, -1, "Hello"); + testText(ids, 0, 1, "H"); + + testCharAfterOffset(ids, 0, "e", 1, 2); + testCharBeforeOffset(ids, 1, "H", 0, 1); + testCharAtOffset(ids, 1, "e", 1, 2); + + ////////////////////////////////////////////////////////////////////////// + // HTML input + + testTextAtOffset([ getNode("tbox1") ], BOUNDARY_LINE_START, + [ [ 0, 4, "test", 0, 4 ] ]); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=396166" + title="xul:label@value accessible should implement nsIAccessibleText"> + Bug 396166 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=899433" + title="Accessibility returns empty line for last line in certain cases"> + Bug 899433 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + <label id="label1" value="Hello"/> + <label id="label2">Hello</label> + + <html:input id="tbox1" value="test"/> + </vbox> +</window> diff --git a/accessible/tests/mochitest/text/test_gettext.html b/accessible/tests/mochitest/text/test_gettext.html new file mode 100644 index 0000000000..2f221a416b --- /dev/null +++ b/accessible/tests/mochitest/text/test_gettext.html @@ -0,0 +1,135 @@ +<!DOCTYPE html> +<html> +<head> + <title>Get text between offsets tests</title> + <meta charset="utf-8"> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // + // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + + var IDs = [ "i1", "d1", "d1wrap", "e1", "t1" ]; + + testCharacterCount(IDs, 15); + + testText(IDs, 0, 1, "h"); + testText(IDs, 1, 3, "el"); + testText(IDs, 14, 15, "d"); + testText(IDs, 0, 15, "hello my friend"); + testText(IDs, 0, -1, "hello my friend"); + + // //////////////////////////////////////////////////////////////////////// + // + // __B__r__a__v__e__ __S__i__r__ __ __R__o__b__i__n__ __ __ __r__a__n + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 + + IDs = [ "i2", "dpre2", "epre2", "t2" ]; + + testCharacterCount(IDs, 22); + + testText(IDs, 0, 1, "B"); + testText(IDs, 5, 6, " "); + testText(IDs, 9, 11, " "); + testText(IDs, 16, 19, " "); + testText(IDs, 0, 22, "Brave Sir Robin ran"); + testText(IDs, 0, -1, "Brave Sir Robin ran"); + + testCharacterCount(["d2", "e2"], 19); + testText(["d2", "e2"], 0, 19, "Brave Sir Robin ran"); + + // //////////////////////////////////////////////////////////////////////// + // + // __o__n__e__w__o__r__d__\n + // 0 1 2 3 4 5 6 7 + // __\n + // 8 + // __t__w__o__ __w__o__r__d__s__\n + // 9 10 11 12 13 14 15 16 17 18 + + IDs = ["d3", "dbr3", "e3", "ebr3", "t3"]; + + testCharacterCount(IDs, 19); + + testText(IDs, 0, 19, "oneword\n\ntwo words\n"); + testText(IDs, 0, -1, "oneword\n\ntwo words\n"); + + // //////////////////////////////////////////////////////////////////////// + // + // CSS text-transform + // + // Content with `text-transform:uppercase | lowercase | capitalize` returns + // the transformed content. + // + testText(["d4a"], 0, -1, "HELLO MY FRIEND"); + testText(["d4b"], 0, -1, "hello my friend"); + testText(["d4c"], 0, -1, "Hello My Friend"); + + // `text-transform: full-width | full-size-kana` should not be reflected in + // a11y. + testText(["d5a"], 0, -1, "hello my friend"); + testText(["d5b"], 0, -1, "ゕゖヵヶ"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="i1" value="hello my friend"/> + <div id="d1">hello my friend</div> + <div id="d1wrap" style="word-wrap:break-word; width:1px">hello my friend</div> + <div id="e1" contenteditable="true">hello my friend</div> + <textarea id="t1">hello my friend</textarea> + + <input id="i2" value="Brave Sir Robin ran"/> + <pre><div id="dpre2">Brave Sir Robin ran</div></pre> + <pre><div id="epre2" contenteditable="true">Brave Sir Robin ran</div></pre> + <textarea id="t2" cols="300">Brave Sir Robin ran</textarea> + <div id="d2">Brave Sir Robin ran</div> + <div id="e2" contenteditable="true">Brave Sir Robin ran</div> + + <pre> + <div id="d3">oneword + +two words +</div> + <div id="dbr3">oneword<br/><br/>two words<br/></div> + <div id="e3" contenteditable="true">oneword + +two words +</div> + <div id="ebr3" contenteditable="true">oneword<br/><br/>two words<br/></div> + <textarea id="t3" cols="300">oneword + +two words +</textarea> + </pre> + + <div id="d4a" style="text-transform:uppercase">Hello My Friend</div> + <div id="d4b" style="text-transform:lowercase">Hello My Friend</div> + <div id="d4c" style="text-transform:capitalize">hello my friend</div> + + <div id="d5a" style="text-transform:full-width">hello my friend</div> + <div id="d5b" style="text-transform:full-size-kana">ゕゖヵヶ</div> + +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_hypertext.html b/accessible/tests/mochitest/text/test_hypertext.html new file mode 100644 index 0000000000..12d94a9f6c --- /dev/null +++ b/accessible/tests/mochitest/text/test_hypertext.html @@ -0,0 +1,149 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessibleText getText related function tests for rich text</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style> + #listitemnone { + list-style-type: none; + } + h6.gencontent:before { + content: "aga" + } + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // null getText + // //////////////////////////////////////////////////////////////////////// + + var emptyTextAcc = getAccessible("nulltext", [nsIAccessibleText]); + is(emptyTextAcc.getText(0, -1), "", "getText() END_OF_TEXT with null string"); + is(emptyTextAcc.getText(0, 0), "", "getText() Len==0 with null string"); + + // //////////////////////////////////////////////////////////////////////// + // hypertext + // //////////////////////////////////////////////////////////////////////// + + // ! - embedded object char + // __h__e__l__l__o__ __!__ __s__e__e__ __!__ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 + + var IDs = [ "hypertext", "hypertext2", "ht_displaycontents" ]; + + // ////////////////////////////////////////////////////////////////////// + // characterCount + + testCharacterCount(IDs, 13); + + // ////////////////////////////////////////////////////////////////////// + // getText + + testText(IDs, 0, 1, "h"); + testText(IDs, 5, 7, " " + kEmbedChar); + testText(IDs, 10, 13, "e " + kEmbedChar); + testText(IDs, 0, 13, "hello " + kEmbedChar + " see " + kEmbedChar); + + // ////////////////////////////////////////////////////////////////////// + // getTextAtOffset line boundary + + testTextAtOffset(0, BOUNDARY_LINE_START, "line ", 0, 5, + "hypertext3", kOk, kOk, kOk); + + // XXX: see bug 634202. + testTextAtOffset(0, BOUNDARY_LINE_START, "line ", 0, 5, + "hypertext4", kTodo, kOk, kTodo); + + // //////////////////////////////////////////////////////////////////////// + // list + // //////////////////////////////////////////////////////////////////////// + + IDs = [ "list" ]; + testCharacterCount(IDs, 2); + testText(IDs, 0, 2, kEmbedChar + kEmbedChar); + + IDs = [ "listitem" ]; + testCharacterCount(IDs, 6); + testText(IDs, 0, 6, "1. foo"); + + IDs = [ "listitemnone" ]; + testCharacterCount(IDs, 3); + testText(IDs, 0, 3, "bar"); + + testText(["testbr"], 0, 3, "foo"); + + testTextAtOffset(2, nsIAccessibleText.BOUNDARY_CHAR, "o", 2, 3, "testbr", + kOk, kOk, kOk); + testTextAtOffset(2, nsIAccessibleText.BOUNDARY_WORD_START, "foo\n", 0, 4, + "testbr", kOk, kOk, kOk); + testTextBeforeOffset(2, nsIAccessibleText.BOUNDARY_LINE_START, "foo\n", + 0, 4, "testbr", kTodo, kOk, kTodo); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Fix getText" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=630001"> + Bug 630001, part3 + </a> + <a target="_blank" + title="getTextAtOffset line boundary may return more than one line" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=638326"> + Bug 638326 + </a> + <a target="_blank" + title="getText(0, -1) fails with empty text" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=749810"> + Bug 749810 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="nulltext"></div> + + <div id="hypertext">hello <a>friend</a> see <img src="about:blank"></div> + <div id="hypertext2">hello <a>friend</a> see <input></div> + <div id="ht_displaycontents">hello <a>friend</a> see <ul id="ul" style="display: contents;"> + <li>Supermarket 1</li> + <li>Supermarket 2</li> + </ul></div> + <ol id="list"> + <li id="listitem">foo</li> + <li id="listitemnone">bar</li> + </ol> + + <div id="hypertext3">line +<!-- haha --> +<!-- hahaha --> +<h6>heading</h6> + </div> + + <div id="hypertext4">line +<!-- haha --> +<!-- hahaha --> +<h6 role="presentation" class="gencontent">heading</h6> + </div> + + <div id="testbr">foo<br/></div> + +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_lineboundary.html b/accessible/tests/mochitest/text/test_lineboundary.html new file mode 100644 index 0000000000..3793a21a75 --- /dev/null +++ b/accessible/tests/mochitest/text/test_lineboundary.html @@ -0,0 +1,341 @@ +<!DOCTYPE html> +<html> +<head> + <title>Line boundary getText* functions tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript"> + function doTest() { + testTextAtOffset("line_test_1", BOUNDARY_LINE_START, + [[0, 6, "Line 1 ", 0, 7], + [7, 7, "", 7, 7], + [8, 15, "Line 3 ", 8, 15]]); + + // //////////////////////////////////////////////////////////////////////// + // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + + var IDs = [ "input", "div", "editable", "textarea", + getNode("ta", getNode("ta_cntr").contentDocument) ]; + + testTextBeforeOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 15, "", 0, 0 ] ]); + testTextBeforeOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 15, "", 0, 0 ] ]); + + testTextAtOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 15, "hello my friend", 0, 15 ] ]); + testTextAtOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 15, "hello my friend", 0, 15 ] ]); + + testTextAfterOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 15, "", 15, 15 ] ]); + testTextAfterOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 15, "", 15, 15 ] ]); + + // //////////////////////////////////////////////////////////////////////// + // __o__n__e__w__o__r__d__\n + // 0 1 2 3 4 5 6 7 + // __\n + // 8 + // __t__w__o__ __w__o__r__d__s__\n + // 9 10 11 12 13 14 15 16 17 18 + + IDs = [ "ml_div", "ml_divbr", "ml_editable", "ml_editablebr", "ml_textarea"]; + + testTextBeforeOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 7, "", 0, 0 ], + [ 8, 8, "oneword\n", 0, 8 ], + [ 9, 18, "\n", 8, 9 ], + [ 19, 19, "two words\n", 9, 19 ]]); + + testTextBeforeOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 7, "", 0, 0 ], + [ 8, 8, "oneword", 0, 7 ], + [ 9, 18, "\n", 7, 8 ], + [ 19, 19, "\ntwo words", 8, 18 ]]); + + testTextAtOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 7, "oneword\n", 0, 8 ], + [ 8, 8, "\n", 8, 9 ], + [ 9, 18, "two words\n", 9, 19 ], + [ 19, 19, "", 19, 19 ]]); + testTextAtOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 7, "oneword", 0, 7 ], + [ 8, 8, "\n", 7, 8 ], + [ 9, 18, "\ntwo words", 8, 18 ], + [ 19, 19, "\n", 18, 19 ]]); + + testTextAfterOffset(IDs, BOUNDARY_LINE_START, + [ [ 0, 7, "\n", 8, 9 ], + [ 8, 8, "two words\n", 9, 19 ], + [ 9, 19, "", 19, 19 ]]); + testTextAfterOffset(IDs, BOUNDARY_LINE_END, + [ [ 0, 7, "\n", 7, 8 ], + [ 8, 8, "\ntwo words", 8, 18 ], + [ 9, 18, "\n", 18, 19 ], + [ 19, 19, "", 19, 19 ]]); + + // //////////////////////////////////////////////////////////////////////// + // a * b (* is embedded char for link) + testTextBeforeOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START, + [ [ 0, 5, "", 0, 0 ] ]); + + testTextBeforeOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END, + [ [ 0, 5, "", 0, 0 ] ]); + + testTextAtOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START, + [ [ 0, 5, "a " + kEmbedChar + " c", 0, 5 ] ]); + + testTextAtOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END, + [ [ 0, 5, "a " + kEmbedChar + " c", 0, 5 ] ]); + + testTextAfterOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_START, + [ [ 0, 5, "", 5, 5 ] ]); + + testTextAfterOffset([ getAccessible("ht_1").firstChild ], BOUNDARY_LINE_END, + [ [ 0, 5, "", 5, 5 ] ]); + + // //////////////////////////////////////////////////////////////////////// + // foo<br> and foo<br><br> + + testTextAtOffset([ getAccessible("ht_2").firstChild.firstChild ], + BOUNDARY_LINE_START, + [ [ 0, 3, "foo\n", 0, 4 ] ]); + testTextAtOffset([ getAccessible("ht_3").firstChild.firstChild ], + BOUNDARY_LINE_START, + [ [ 0, 3, "foo\n", 0, 4 ], [ 4, 4, "\n", 4, 5 ] ]); + + // //////////////////////////////////////////////////////////////////////// + // 'Hello world ' (\n is rendered as space) + + testTextAtOffset([ "ht_4" ], BOUNDARY_LINE_START, + [ [ 0, 12, "Hello world ", 0, 12 ] ]); + + // //////////////////////////////////////////////////////////////////////// + // list items + + testTextAtOffset([ "li1" ], BOUNDARY_LINE_START, + [ [ 0, 6, kDiscBulletText + "Item", 0, 6 ] ]); + testTextAtOffset([ "li2" ], BOUNDARY_LINE_START, + [ [ 0, 2, kDiscBulletText, 0, 2 ] ]); + testTextAtOffset([ "li3" ], BOUNDARY_LINE_START, + [ [ 0, 8, kDiscBulletText + "a long ", 0, 9 ], + [ 9, 12, "and ", 9, 13 ] ]); + testTextAtOffset([ "li4" ], BOUNDARY_LINE_START, + [ [ 0, 7, kDiscBulletText + "a " + kEmbedChar + " c", 0, 7 ] ]); + testTextAtOffset([ "li5" ], BOUNDARY_LINE_START, + [ [ 0, 2, kDiscBulletText + "\n", 0, 3 ], + [ 3, 7, "hello", 3, 8 ] ]); + testTextAtOffset([ "ul1" ], BOUNDARY_LINE_START, + [ [ 0, 0, kEmbedChar, 0, 1 ], + [ 1, 1, kEmbedChar, 1, 2 ], + [ 2, 2, kEmbedChar, 2, 3 ], + [ 3, 3, kEmbedChar, 3, 4 ], + [ 4, 5, kEmbedChar, 4, 5 ] ]); + + testTextAtOffset([ "li6" ], BOUNDARY_LINE_START, + [ [ 0, 7, "1. Item", 0, 7 ] ]); + testTextAtOffset([ "li7" ], BOUNDARY_LINE_START, + [ [ 0, 3, "2. ", 0, 3 ] ]); + testTextAtOffset([ "li8" ], BOUNDARY_LINE_START, + [ [ 0, 9, "3. a long ", 0, 10 ], + [ 10, 13, "and ", 10, 14 ] ]); + testTextAtOffset([ "li9" ], BOUNDARY_LINE_START, + [ [ 0, 8, "4. a " + kEmbedChar + " c", 0, 8 ] ]); + testTextAtOffset([ "li10" ], BOUNDARY_LINE_START, + [ [ 0, 3, "5. \n", 0, 4 ], + [ 4, 8, "hello", 4, 9 ] ]); + testTextAtOffset([ "ol1" ], BOUNDARY_LINE_START, + [ [ 0, 0, kEmbedChar, 0, 1 ], + [ 1, 1, kEmbedChar, 1, 2 ], + [ 2, 2, kEmbedChar, 2, 3 ], + [ 3, 3, kEmbedChar, 3, 4 ], + [ 4, 5, kEmbedChar, 4, 5 ] ]); + + // //////////////////////////////////////////////////////////////////////// + // Nested hypertexts + + testTextAtOffset(["ht_5" ], BOUNDARY_LINE_START, + [ [ 0, 0, kEmbedChar, 0, 1 ] ]); + + // //////////////////////////////////////////////////////////////////////// + // Block followed by list + + testTextAtOffset([ "block_then_ul" ], BOUNDARY_LINE_START, + [ [ 0, 0, kEmbedChar, 0, 1 ], + [ 1, 1, kEmbedChar, 1, 2 ] ]); + + // Embedded char containing a line break breaks line offsets in parent. + testTextAtOffset([ "brInEmbed" ], BOUNDARY_LINE_START, + [ [0, 1, "a " + kEmbedChar, 0, 3], + [2, 2, "a " + kEmbedChar + " d", 0, 5], + [3, 5, kEmbedChar + " d", 2, 5] ]); + testTextAtOffset([ "brInEmbedAndBefore" ], BOUNDARY_LINE_START, + [ [0, 1, "a\n", 0, 2], + [2, 3, "b " + kEmbedChar, 2, 5], + [4, 4, "b " + kEmbedChar + " e", 2, 7], + [5, 7, kEmbedChar + " e", 4, 7] ]); + testTextAtOffset([ "brInEmbedAndAfter" ], BOUNDARY_LINE_START, + [ [0, 1, "a " + kEmbedChar, 0, 3], + [2, 2, "a " + kEmbedChar + " d\n", 0, 6], + [3, 5, kEmbedChar + " d\n", 2, 6], + [6, 7, "e", 6, 7] ]); + testTextAtOffset([ "brInEmbedAndBlockElementAfter" ], BOUNDARY_LINE_START, + [ [0, 2, "a " + kEmbedChar, 0, 3], + [3, 4, kEmbedChar, 3, 4] ]); + testTextAtOffset([ "brInEmbedThenTextThenBlockElement" ], BOUNDARY_LINE_START, + [ [0, 1, "a " + kEmbedChar, 0, 3], + [2, 2, "a " + kEmbedChar + " d", 0, 5], + [3, 4, kEmbedChar + " d", 2, 5], + [5, 6, kEmbedChar, 5, 6] ]); + testTextAtOffset([ "noBrInEmbedButOneBefore" ], BOUNDARY_LINE_START, + [ [0, 1, "a\n", 0, 2], + [2, 7, "b " + kEmbedChar + " d", 2, 7] ]); + testTextAtOffset([ "noBrInEmbedButOneAfter" ], BOUNDARY_LINE_START, + [ [0, 3, "a " + kEmbedChar + "\n", 0, 4], + [4, 5, "c", 4, 5] ]); + testTextAtOffset([ "twoEmbedsWithBRs" ], BOUNDARY_LINE_START, + [ [0, 1, "a " + kEmbedChar, 0, 3], + [2, 2, "a " + kEmbedChar + kEmbedChar, 0, 4], + [3, 3, kEmbedChar + kEmbedChar + " f", 2, 6], + [4, 6, kEmbedChar + " f", 3, 6] ]); + + // Inline block span with nested spans and BRs + testTextAtOffset([ "inlineBlockWithSpansAndBrs" ], BOUNDARY_LINE_START, + [ [0, 1, "a\n", 0, 2], + [2, 3, "b\n", 2, 4], + [4, 5, "c", 4, 5] ]); + + // Spans with BRs and whitespaces. + testTextAtOffset([ "spansWithWhitespaces" ], BOUNDARY_LINE_START, + [ [0, 6, "Line 1\n", 0, 7], + [7, 13, "Line 2\n", 7, 14], + [14, 20, "Line 3\n", 14, 21], + [21, 27, "Line 4\n", 21, 28], + [28, 28, "", 28, 28] ]); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="getTextAtOffset for word boundaries: beginning of a new life" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=853340"> + Bug 853340 + </a> + <a target="_blank" + title="getTextBeforeOffset for word boundaries: evolving" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=855732"> + Bug 855732 + </a> + <a target="_blank" + title=" getTextAfterOffset for line boundary on new rails" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=882292"> + Bug 882292 + </a> + <a target="_blank" + title="getTextAtOffset broken for last object when closing tag is preceded by newline char" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=947170"> + Bug 947170 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input" value="hello my friend"/> + <div id="div">hello my friend</div> + <div id="editable" contenteditable="true">hello my friend</div> + <textarea id="textarea">hello my friend</textarea> + <iframe id="ta_cntr" + src="data:text/html,<html><body><textarea id='ta'>hello my friend</textarea></body></html>"></iframe> + + <pre> + <div id="ml_div" style="border-style:outset;">oneword + +two words +</div> + <div id="ml_divbr" style="border-style:outset;">oneword<br/><br/>two words<br/></div> + <div id="ml_editable" style="border-style:outset;" contenteditable="true">oneword + +two words +</div> + <div id="ml_editablebr" contenteditable="true" style="border-style:outset;">oneword<br/><br/>two words<br/></div> + <textarea id="ml_textarea" cols="300">oneword + +two words +</textarea> + </pre> + + <iframe id="ht_1" src="data:text/html,<html><body>a <a href=''>b</a> c</body></html>"></iframe> + + <iframe id="ht_2" src="data:text/html,<div contentEditable='true'>foo<br/></div>"></iframe> + <iframe id="ht_3" src="data:text/html,<div contentEditable='true'>foo<br/><br/></div>"></iframe> + + <p id="ht_4">Hello world +</p> + + <ul id="ul1"> + <li id="li1">Item</li> + <li id="li2"></li> + <li id="li3" style="font-family:monospace; font-size:10pt; width:8ch;">a long and winding road that lead me to your door</li> + <li id="li4">a <a href=''>b</a> c</li> + <li id="li5"><br>hello</li> + </ul> + + <ol id="ol1"> + <li id="li6">Item</li> + <li id="li7"></li> + <li id="li8" style="font-family:monospace; font-size:10pt; width:8ch;">a long and winding road that lead me to your door</li> + <li id="li9">a <a href=''>b</a> c</li> + <li id="li10"><br>hello</li> + </ol> + + <div id="ht_5"> + <div> + <p>sectiounus</p> + <p>seciofarus</p> + </div> + </div> + <div id="line_test_1"> + Line 1 + <center><input type="TEXT"><input value="Button" type="SUBMIT"></center> + Line 3 + </div> + + <div id="block_then_ul"> + <p>Block</p> + <ul><li>Li</li></ul> + </div> + <div id="brInEmbed" contenteditable>a <a href="https://mozilla.org/">b<br>c</a> d</div> + <div id="brInEmbedAndBefore">a<br>b <a href="https://mozilla.org/">c<br>d</a> e</div> + <div id="brInEmbedAndAfter">a <a href="https://mozilla.org/">b<br>c</a> d<br>e</div> + <div id="brInEmbedAndBlockElementAfter">a <a href="https://mozilla.org/">b<br>c</a><p>d</p></div> + <div id="brInEmbedThenTextThenBlockElement">a <a href="https://mozilla.org/">b<br>c</a> d<p>e</p></div> + <div id="noBrInEmbedButOneBefore">a<br>b <a href="https://mozilla.org/">c</a> d</div> + <div id="noBrInEmbedButOneAfter">a <a href="https://mozilla.org/">b</a><br>c</div> + <div id="twoEmbedsWithBRs">a <a href="https://mozilla.org">b<br>c</a><a href="https://mozilla.org">d<br>e</a> f</div> + <span id="inlineBlockWithSpansAndBrs" style="display: inline-block;"><span>a<br>b<br><span></span></span>c</span> + <div id="spansWithWhitespaces"> <!-- Don't indent the following block --> +<span>Line 1<br/> +</span> +<span>Line 2<br/> +</span> +<span>Line 3<br/> +</span> +<span>Line 4<br/> +</span></div><!-- OK to indent again --> +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_paragraphboundary.html b/accessible/tests/mochitest/text/test_paragraphboundary.html new file mode 100644 index 0000000000..9a9f546e96 --- /dev/null +++ b/accessible/tests/mochitest/text/test_paragraphboundary.html @@ -0,0 +1,140 @@ +<!DOCTYPE html> +<html> +<head> + <title>Paragraph boundary getText* functions tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript"> + function doTest() { + // First, test the contentEditable. + testTextAtOffset("ce", BOUNDARY_PARAGRAPH, + [[0, 0, kEmbedChar, 0, 1], + [1, 2, kEmbedChar, 1, 2]]); + + // Now, test each paragraph. + var ID = getNode("ce").firstElementChild; + testTextAtOffset(ID, BOUNDARY_PARAGRAPH, + [[0, 15, "hello my friend", 0, 15]]); + ID = getNode("ce").lastElementChild; + testTextAtOffset(ID, BOUNDARY_PARAGRAPH, + [[0, 11, "hello again", 0, 11]]); + + // Test a paragraph whose line forcefully wraps. + testTextAtOffset("forced_wrap", BOUNDARY_PARAGRAPH, + [[0, 2, "ab", 0, 2]]); + + // Test paragraphs with a few line breaks. + testTextAtOffset("forced_br", BOUNDARY_PARAGRAPH, + [[0, 1, "a\n", 0, 2], // a and br treated as a paragraph + [2, 3, "b\n", 2, 4], // b treated as a paragraph, excl 2nd line break + [4, 4, "\n", 4, 5], // second br treated as a separate paragraph + [5, 6, "c", 5, 6]]); // Last paragraph treated as usual + testTextAtOffset("br_at_beginning", BOUNDARY_PARAGRAPH, + [[0, 0, "\n", 0, 1], // br treated as a separate paragraph + [1, 2, "a\n", 1, 3], // a and br treated as a paragraph + [3, 4, "b", 3, 4]]); // b treated as last paragraph + + // Test a paragraph with an embedded link. + testTextAtOffset("pWithLink", BOUNDARY_PARAGRAPH, + [[0, 3, "a" + kEmbedChar + "d", 0, 3]]); + testTextAtOffset("link", BOUNDARY_PARAGRAPH, + [[0, 2, "bc", 0, 2]]); + + // Paragraph with link that contains a line break. + testTextAtOffset("pWithLinkWithBr", BOUNDARY_PARAGRAPH, + [[0, 0, "a" + kEmbedChar, 0, 2], + [1, 1, "a" + kEmbedChar + "d", 0, 3], + [2, 3, kEmbedChar + "d", 1, 3]]); + + // Test a list and list item + testTextAtOffset("ul", BOUNDARY_PARAGRAPH, + [[0, 0, kEmbedChar, 0, 1], + [1, 2, kEmbedChar, 1, 2]]); + testTextAtOffset("li1", BOUNDARY_PARAGRAPH, + [[0, 3, "• a", 0, 3]]); + + // Test line breaks in a textarea. + testTextAtOffset("textarea", BOUNDARY_PARAGRAPH, + [[0, 1, "a\n", 0, 2], + [2, 3, "b\n", 2, 4], + [4, 4, "\n", 4, 5], + [5, 6, "c", 5, 6]]); + + // Test that a textarea has a blank paragraph at the end if it contains + // a line break as its last character. + testTextAtOffset("textarea_with_trailing_br", BOUNDARY_PARAGRAPH, + [[0, 15, "This is a test.\n", 0, 16], + [16, 16, "", 16, 16]]); + + // Paragraph with a presentational line break. + testTextAtOffset("presentational_br", BOUNDARY_PARAGRAPH, + [[0, 3, "a b", 0, 3]]); + + // Two paragraphs in a div, non-editable case. + testTextAtOffset("two_paragraphs", BOUNDARY_PARAGRAPH, + [[0, 0, kEmbedChar, 0, 1], + [1, 2, kEmbedChar, 1, 2]]); + + // Div containing a paragraph containing a link + testTextAtOffset("divWithParaWithLink", BOUNDARY_PARAGRAPH, + [[0, 0, kEmbedChar, 0, 1], + [1, 2, "b", 1, 2]]); + + // Two text nodes and a br + testTextAtOffset("twoTextNodesAndBr", BOUNDARY_PARAGRAPH, + [[0, 2, "ab\n", 0, 3], + [3, 3, "", 3, 3]]); + + // Link followed by a paragraph. + testTextAtOffset("linkThenPara", BOUNDARY_PARAGRAPH, + [[0, 0, kEmbedChar, 0, 1], + [1, 2, kEmbedChar, 1, 2]]); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="getTextAtOffset for paragraph boundaries" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1520779"> + Bug 1520779 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="ce" contenteditable="true"> + <p>hello my friend</p> + <p>hello again</p> + </div> + <p id="forced_wrap" style="width: 1px; word-break: break-all;">ab</p> + <p id="forced_br">a<br>b<br><br>c</p> + <p id="br_at_beginning"><br>a<br>b</p> + <p id="pWithLink">a<a id="link" href="https://example.com/">bc</a>d</p> + <p id="pWithLinkWithBr">a<a href="#">b<br>c</a>d</p> + <ul id="ul"><li id="li1">a</li><li>b</li></ul> + <textarea id="textarea">a +b + +c</textarea> <!-- This must be outdented for a correct test case --> + <textarea id="textarea_with_trailing_br">This is a test. +</textarea> <!-- This must be outdented for a correct test case --> + <p id="presentational_br" style="white-space: pre-wrap;">a<span> <br role="presentation"></span>b</p> + <div id="two_paragraphs"><p>a</p><p>b</p></div> + <div id ="divWithParaWithLink"><p><a href="#">a</a></p>b</div> + <p id="twoTextNodesAndBr">a<span>b</span><br></p> + <div id="linkThenPara"><a href="#">a</a><p>b</p></div> +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_passwords.html b/accessible/tests/mochitest/text/test_passwords.html new file mode 100644 index 0000000000..fc184f2d45 --- /dev/null +++ b/accessible/tests/mochitest/text/test_passwords.html @@ -0,0 +1,72 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessibleText getText related function tests for text and password inputs</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // regular text and password inputs + // //////////////////////////////////////////////////////////////////////// + + // ////////////////////////////////////////////////////////////////////// + // characterCount and getText for regular text field + + var IDs = [ "username" ]; + testCharacterCount(IDs, 4); + testText(IDs, 0, 4, "test"); + + // ////////////////////////////////////////////////////////////////////// + // characterCount and getText for password field + + IDs = [ "password" ]; + testCharacterCount(IDs, 4); + let password = document.getElementById("password"); + let editor = SpecialPowers.wrap(password).editor; + let passwordMask = editor.passwordMask; + testText(IDs, 0, 4, `${passwordMask}${passwordMask}${passwordMask}${passwordMask}`); + // a11y data is updated at next tick so that we need to refresh here. + editor.unmask(0, 2); + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0); + testText(IDs, 0, 4, `te${passwordMask}${passwordMask}`); + editor.unmask(2, 4); + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0); + testText(IDs, 0, 4, `${passwordMask}${passwordMask}st`); + editor.unmask(0, 4); + SpecialPowers.DOMWindowUtils.advanceTimeAndRefresh(0); + testText(IDs, 0, 4, `test`); + SpecialPowers.DOMWindowUtils.restoreNormalRefresh(); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="mochitest for getText for password fields" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=415943">Mozilla Bug 415943</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <form action="post.php" method="post"> + <label for="username">User name:</label> + <input id="username" value="test"><br /> + <label for="password">Password:</label> + <input type="password" id="password" value="test"/> + </form> +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_selection.html b/accessible/tests/mochitest/text/test_selection.html new file mode 100644 index 0000000000..ef4677472c --- /dev/null +++ b/accessible/tests/mochitest/text/test_selection.html @@ -0,0 +1,96 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test text selection functions</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + + <script type="application/javascript"> + + function doTest() { + // Test selection count: clean selection / check count. + testTextAddSelection("div0", 0, 2, 1); // |Test selection... + cleanTextSelections("div0"); + testTextSelectionCount("div0", 0); + + // Test addition: adding two equal selections, the second one should + // not be added. + testTextAddSelection("div1", 7, 9, 1); // Test ad|di|ng two... + testTextAddSelection("div1", 7, 9, 1); // Test ad|di|ng two... + testTextGetSelection("div1", 7, 9, 0); + + // Test overlapping selections: adding three selections, one adjacent. + testTextAddSelection("div2", 0, 3, 1); // |Tes|t adding 3... + testTextAddSelection("div2", 7, 9, 2); // |Tes|t ad|di|ng 3... + testTextAddSelection("div2", 3, 4, 3); // |Tes||t| ad|di|ng 3... + testTextGetSelection("div2", 0, 3, 0); + testTextGetSelection("div2", 3, 4, 1); + testTextGetSelection("div2", 7, 9, 2); + + // Test selection re-ordering: adding two selections. + // NOTE: removeSelections aSelectionIndex is from start of document. + testTextAddSelection("div3", 0, 3, 1); // |Tes|t adding 2... + testTextAddSelection("div3", 7, 9, 2); // |Tes|t ad|di|ng 2... + testTextRemoveSelection("div3", 4, 1); // Test ad|di|ng 2... + + // Test extending existing selection. + // NOTE: setSelectionBounds aSelectionIndex is from start of document. + testTextAddSelection("div4", 4, 5, 1); // Test| |extending... + testTextSetSelection("div4", 4, 9, 6, 1); // Test| exte|nding... + + // Test moving an existing selection. + // NOTE: setSelectionBounds aSelectionIndex is from start of document. + testTextAddSelection("div5", 1, 3, 1); // T|es|t moving... + testTextSetSelection("div5", 5, 9, 6, 1); // Test |movi|ng... + + // Test adding selections to multiple inner elements. + testTextAddSelection("div71", 0, 3, 1); // |Tes|t adding... + testTextAddSelection("div71", 7, 8, 2); // |Tes|t ad|d|ing... + testTextAddSelection("div72", 4, 6, 1); // Test| a|dding... + testTextAddSelection("div72", 7, 8, 2); // Test| a|d|d|ing... + + // Test adding selection to parent element. + // NOTE: If inner elements are represented as embedded chars + // we count their internal selections. + testTextAddSelection("div7", 7, 8, 5); // Test ad|d|ing... + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + +</script> +</head> + +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="div0">Test selection count</div> + </br> + <div id="div1">Test adding two equal selections </div> + <div id="div2">Test adding 3 selections one adjacent </div> + <div id="div3">Test adding 2 selections, remove first one </div> + <div id="div4">Test extending a selection </div> + <div id="div5">Test moving a selection </div> + </br> + <div id="div7">Test adding selections to parent element + <div id="div71">Test adding selections to inner element1 </div> + <div id="div72">Test adding selections to inner element2 </div> + </div> + +</body> + +</html> diff --git a/accessible/tests/mochitest/text/test_settext_input_event.html b/accessible/tests/mochitest/text/test_settext_input_event.html new file mode 100644 index 0000000000..2f0ecacf30 --- /dev/null +++ b/accessible/tests/mochitest/text/test_settext_input_event.html @@ -0,0 +1,38 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test that setTextContents only sends one DOM input event</title> + <meta charset="utf-8" /> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript"> + async function doTest() { + let input = getAccessible("input", [nsIAccessibleEditableText]); + let eventPromise = new Promise(resolve => + document.getElementById("input").addEventListener( + "input", resolve, { once: true })); + + input.setTextContents("goodbye"); + let inputEvent = await eventPromise; + is(inputEvent.target.value, "goodbye", "input set to new value."); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="HyperTextAccessible::ReplaceText causes two distinct DOM 'input' events" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1490840">Mozilla Bug 1490840</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <input id="input" value="hello"> +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_textBounds.html b/accessible/tests/mochitest/text/test_textBounds.html new file mode 100644 index 0000000000..66e7f1a93f --- /dev/null +++ b/accessible/tests/mochitest/text/test_textBounds.html @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> +<head> + <title>TextBounds tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + + <script type="application/javascript"> + function doTest() { + // Returned rect should be all 0 if no frame; e.g. display: contents. + testTextBounds( + "displayContents", 0, 0, [0, 0, 0, 0], COORDTYPE_SCREEN_RELATIVE + ); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <section id="displayContents" style="display: contents;"></section> +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_wordboundary.html b/accessible/tests/mochitest/text/test_wordboundary.html new file mode 100644 index 0000000000..b3eb5dc0f4 --- /dev/null +++ b/accessible/tests/mochitest/text/test_wordboundary.html @@ -0,0 +1,361 @@ +<!DOCTYPE html> +<html> +<head> + <title>Word boundary text tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + + <script type="application/javascript"> + function doTest() { + // "hello" + // __h__e__l__l__o__ + // 0 1 2 3 4 5 + var ids = [ "i1", "d1", "e1", "t1" ]; + testTextBeforeOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "", 0, 0 ] ]); + testTextBeforeOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, "", 0, 0 ] ]); + + testTextAtOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "hello", 0, 5 ] ]); + testTextAtOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, "hello", 0, 5 ] ]); + + testTextAfterOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "", 5, 5 ] ]); + testTextAfterOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, "", 5, 5 ] ]); + + // "hello " + // __h__e__l__l__o__ __ + // 0 1 2 3 4 5 6 + ids = [ "i2", "d2", "p2", "e2", "t2" ]; + testTextBeforeOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 6, "", 0, 0 ] ]); + testTextBeforeOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, "", 0, 0 ], + [ 6, 6, "hello", 0, 5 ], + ]); + + testTextAtOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 6, "hello ", 0, 6 ] ]); + testTextAtOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 4, "hello", 0, 5 ], + [ 5, 6, " ", 5, 6 ], + ]); + + testTextAfterOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 6, "", 6, 6 ] ]); + testTextAfterOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, " ", 5, 6 ], + [ 6, 6, "", 6, 6 ], + ]); + + // "hello all" + // __h__e__l__l__o__ __a__l__l__ + // 0 1 2 3 4 5 6 7 8 9 + ids = [ "i6", "d6", "e6", "t6" ]; + testTextBeforeOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "", 0, 0 ], + [ 6, 9, "hello ", 0, 6 ]]); + testTextBeforeOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, "", 0, 0 ], + [ 6, 9, "hello", 0, 5 ] ]); + + testTextAtOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "hello ", 0, 6 ], + [ 6, 9, "all", 6, 9 ] ]); + testTextAtOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 4, "hello", 0, 5 ], + [ 5, 9, " all", 5, 9 ] ]); + + testTextAfterOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "all", 6, 9 ], + [ 6, 9, "", 9, 9 ] ]); + testTextAfterOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, " all", 5, 9 ], + [ 6, 9, "", 9, 9 ] ]); + + // " hello all " (with whitespace collapsing) + // __h__e__l__l__o__ __a__l__l__ __ + // 0 1 2 3 4 5 6 7 8 9 10 + ids = [ "d6a", "e6a" ]; + testTextBeforeOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "", 0, 0 ], + [ 6, 10, "hello ", 0, 6 ] ]); + testTextBeforeOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, "", 0, 0 ], + [ 6, 9, "hello", 0, 5 ], + [ 10, 10, " all", 5, 9 ] ]); + + testTextAtOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "hello ", 0, 6 ], + [ 6, 10, "all ", 6, 10 ] ]); + testTextAtOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 4, "hello", 0, 5 ], + [ 5, 8, " all", 5, 9 ], + [ 9, 10, " ", 9, 10 ] ]); + + testTextAfterOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "all ", 6, 10 ], + [ 6, 10, "", 10, 10 ] ]); + testTextAfterOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, " all", 5, 9 ], + [ 6, 9, " ", 9, 10 ], + [ 10, 10, "", 10, 10 ] ]); + + // "hello my friend" + // __h__e__l__l__o__ __m__y__ __f__r__i__e__n__d__ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 + ids = [ "i7", "d7", "e7", "t7", "w7" ]; + testTextBeforeOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "", 0, 0 ], + [ 6, 8, "hello ", 0, 6 ], + [ 9, 15, "my ", 6, 9 ] ]); + testTextBeforeOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, "", 0, 0 ], + [ 6, 8, "hello", 0, 5 ], + [ 9, 15, " my", 5, 8 ] ]); + + testTextAtOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "hello ", 0, 6 ], + [ 6, 8, "my ", 6, 9 ], + [ 9, 15, "friend", 9, 15] ]); + testTextAtOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 4, "hello", 0, 5 ], + [ 5, 7, " my", 5, 8 ], + [ 8, 15, " friend", 8, 15] ]); + + testTextAfterOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "my ", 6, 9 ], + [ 6, 8, "friend", 9, 15 ], + [ 9, 15, "", 15, 15 ] ]); + testTextAfterOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, " my", 5, 8 ], + [ 6, 8, " friend", 8, 15 ], + [ 9, 15, "", 15, 15 ] ]); + + // "Brave Sir Robin ran" + // __B__r__a__v__e__ __S__i__r__ __ __R__o__b__i__n__ __ __ __r__a__n__ + // 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 + ids = [ "i8", "d8", "e8", "t8" ]; + testTextBeforeOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "", 0, 0 ], + [ 6, 10, "Brave ", 0, 6 ], + [ 11, 18, "Sir ", 6, 11 ], + [ 19, 22, "Robin ", 11, 19 ] ]); + testTextBeforeOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, "", 0, 0 ], + [ 6, 9, "Brave", 0, 5 ], + [ 10, 16, " Sir", 5, 9 ], + [ 17, 22, " Robin", 9, 16 ] ]); + + testTextAtOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "Brave ", 0, 6 ], + [ 6, 10, "Sir ", 6, 11 ], + [ 11, 18, "Robin ", 11, 19 ], + [ 19, 22, "ran", 19, 22 ] ]); + testTextAtOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 4, "Brave", 0, 5 ], + [ 5, 8, " Sir", 5, 9 ], + [ 9, 15, " Robin", 9, 16 ], + [ 16, 22, " ran", 16, 22 ] ]); + + testTextAfterOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 5, "Sir ", 6, 11 ], + [ 6, 10, "Robin ", 11, 19 ], + [ 11, 18, "ran", 19, 22 ], + [ 19, 22, "", 22, 22 ] ]); + testTextAfterOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 5, " Sir", 5, 9 ], + [ 6, 9, " Robin", 9, 16 ], + [ 10, 16, " ran", 16, 22 ], + [ 17, 22, "", 22, 22 ] ]); + + // 'oneword + // ' + // 'two words + // ' + // __o__n__e__w__o__r__d__\n + // 0 1 2 3 4 5 6 7 + // __\n + // 8 + // __t__w__o__ __w__o__r__d__s__\n__ + // 9 10 11 12 13 14 15 16 17 18 19 + + ids = ["ml_div1", "ml_divbr1", "ml_ediv1", "ml_edivbr1", "ml_t1"]; + testTextBeforeOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 8, "", 0, 0 ], + [ 9, 12, "oneword\n\n", 0, 9 ], + [ 13, 19, "two ", 9, 13 ] ]); + testTextBeforeOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 7, "", 0, 0 ], + [ 8, 12, "oneword", 0, 7, + [ [ 8, "ml_divbr1", kTodo, kOk, kTodo ], + [ 8, "ml_edivbr1", kTodo, kOk, kTodo ], + [ 9, "ml_divbr1", kTodo, kOk, kTodo ], + [ 9, "ml_edivbr1", kTodo, kOk, kTodo ] ] ], + [ 13, 18, "\n\ntwo", 7, 12 ], + [ 19, 19, " words", 12, 18, + [ [ 19, "ml_divbr1", kTodo, kTodo, kTodo ], + [ 19, "ml_edivbr1", kTodo, kTodo, kTodo ] ] ], + ] ); + + testTextAtOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 8, "oneword\n\n", 0, 9 ], + [ 9, 12, "two ", 9, 13 ], + [ 13, 19, "words\n", 13, 19 ] ]); + testTextAtOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 6, "oneword", 0, 7 ], + [ 7, 11, "\n\ntwo", 7, 12 ], + [ 12, 17, " words", 12, 18 ], + [ 18, 19, "\n", 18, 19, + [ [ 18, "ml_divbr1", kTodo, kTodo, kOk ], + [ 18, "ml_edivbr1", kTodo, kTodo, kOk ], + [ 19, "ml_divbr1", kTodo, kTodo, kOk ], + [ 19, "ml_edivbr1", kTodo, kTodo, kOk ] ] ] ]); + + testTextAfterOffset(ids, BOUNDARY_WORD_START, + [ [ 0, 8, "two ", 9, 13 ], + [ 9, 12, "words\n", 13, 19 ], + [ 13, 19, "", 19, 19 ] ]); + testTextAfterOffset(ids, BOUNDARY_WORD_END, + [ [ 0, 7, "\n\ntwo", 7, 12 ], + [ 8, 12, " words", 12, 18 ], + [ 13, 18, "\n", 18, 19, + [ [ 18, "ml_divbr1", kTodo, kTodo, kOk ], + [ 18, "ml_edivbr1", kTodo, kTodo, kOk ] ] ], + [ 19, 19, "", 19, 19 ] ]); + + // a <a href="#">b</a> + // a * + testTextBeforeOffset("cntr_1", BOUNDARY_WORD_START, + [ [ 0, 1, "", 0, 0 ], + [ 2, 3, "a ", 0, 2 ] ]); + + testTextAtOffset("cntr_1", BOUNDARY_WORD_START, + [ [ 0, 1, "a ", 0, 2 ], + [ 2, 3, kEmbedChar, 2, 3 ] ]); + testTextAfterOffset("cntr_1", BOUNDARY_WORD_START, + [ [ 0, 1, kEmbedChar, 2, 3 ], + [ 2, 3, "", 3, 3 ] ]); + + // Punctuation tests. + testTextAtOffset("punc_alone", BOUNDARY_WORD_START, [ + [ 0, 1, "a ", 0, 2 ], + [ 2, 4, "@@ ", 2, 5 ], + [ 5, 6, "b", 5, 6 ] + ]); + testTextAtOffset("punc_begin", BOUNDARY_WORD_START, [ + [ 0, 1, "a ", 0, 2 ], + [ 2, 5, "@@b ", 2, 6 ], + [ 6, 7, "c", 6, 7 ] + ]); + testTextAtOffset("punc_end", BOUNDARY_WORD_START, [ + [ 0, 1, "a ", 0, 2 ], + [ 2, 5, "b@@ ", 2, 6 ], + [ 6, 7, "c", 6, 7 ] + ]); + testTextAtOffset("punc_middle", BOUNDARY_WORD_START, [ + [ 0, 1, "a ", 0, 2 ], + [ 2, 4, "b@@", 2, 5 ], + [ 5, 6, "c ", 5, 7 ], + [ 7, 8, "d", 7, 8 ] + ]); + testTextAtOffset("punc_everywhere", BOUNDARY_WORD_START, [ + [ 0, 1, "a ", 0, 2 ], + [ 2, 6, "@@b@@", 2, 7 ], + [ 7, 10, "c@@ ", 7, 11 ], + [ 11, 12, "d", 11, 12 ] + ]); + + // Multi-word embedded object test. + testTextAtOffset("multiword_embed", BOUNDARY_WORD_START, [ + [ 0, 1, "a ", 0, 2 ], + [ 2, 3, `${kEmbedChar} `, 2, 4, [ + // Word at offset 2 returns end offset 3, should be 4. + [ 2, "multiword_embed", kTodo, kOk, kTodo ] + ] ], + [ 4, 5, "b", 4, 5 ] + ]); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="i1" value="hello"/> + <div id="d1">hello</div> + <div id="e1" contenteditable="true">hello</div> + <textarea id="t1">hello</textarea> + + <input id="i2" value="hello "/> + <div id="d2"> hello </div> + <pre><div id="p2">hello </div></pre> + <div id="e2" contenteditable="true" style='white-space:pre'>hello </div> + <textarea id="t2">hello </textarea> + + <input id="i6" value="hello all"/> + <div id="d6"> hello all</div> + <div id="e6" contenteditable="true">hello all</div> + <textarea id="t6">hello all</textarea> + + <div id="d6a"> hello all </div> + <div id="e6a" contenteditable="true"> hello all </div> + + <input id="i7" value="hello my friend"/> + <div id="d7"> hello my friend</div> + <div id="e7" contenteditable="true">hello my friend</div> + <textarea id="t7">hello my friend</textarea> + <div id="w7" style="width:1em"> hello my friend</div> + + <input id="i8" value="Brave Sir Robin ran"/> + <pre> + <div id="d8">Brave Sir Robin ran</div> + <div id="e8" contenteditable="true">Brave Sir Robin ran</div> + </pre> + <textarea id="t8" cols="300">Brave Sir Robin ran</textarea> + + <pre> +<div id="ml_div1">oneword + +two words +</div> +<div id="ml_divbr1">oneword<br/><br/>two words<br/></div> +<div id="ml_ediv1" contenteditable="true">oneword + +two words +</div> +<div id="ml_edivbr1" contenteditable="true">oneword<br/><br/>two words<br/></div> +<textarea id="ml_t1" cols="300">oneword + +two words +</textarea> + </pre> + + <div id="cntr_1">a <a href="#">b</a></div> + + <p id="punc_alone">a @@ b</p> + <p id="punc_begin">a @@b c</p> + <p id="punc_end">a b@@ c</p> + <p id="punc_middle">a b@@c d</p> + <p id="punc_everywhere">a @@b@@c@@ d</p> + + <p id="multiword_embed">a <a href="#">x y</a> b</p> +</body> +</html> diff --git a/accessible/tests/mochitest/text/test_words.html b/accessible/tests/mochitest/text/test_words.html new file mode 100644 index 0000000000..7e4fba9154 --- /dev/null +++ b/accessible/tests/mochitest/text/test_words.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<html> +<head> + <title>nsIAccessibleText getText related function tests for html:input,html:div and html:textarea</title> + <meta charset="utf-8" /> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript"> + if (navigator.platform.startsWith("Mac")) { + SimpleTest.expectAssertions(0, 1); + } else { + SimpleTest.expectAssertions(0, 1); + } + + function doTest() { + // "one two" + testWords("div1", ["one", "two"]); + + // "one two" + testWords("div2", ["one", "two"]); + + // "one,two" + testWordCount("div3", 2, kOk); + testWordAt("div3", 0, "one", kTodo); + testWordAt("div3", 1, "two", kOk); + + // "one, two" + testWordCount("div4", 2, kOk); + testWordAt("div4", 0, "one", kTodo); + testWordAt("div4", 1, "two", kOk); + + // "one+two" + testWordCount("div5", 2, kOk); + testWordAt("div5", 0, "one", kTodo); + testWordAt("div5", 1, "two", kOk); + + // "one+two " + testWordCount("div6", 2, kOk); + testWordAt("div6", 0, "one", kTodo); + testWordAt("div6", 1, "two", kOk); + + // "one\ntwo" + testWordCount("div7", 2, kOk); + testWordAt("div7", 0, "one", kOk); + testWordAt("div7", 1, "two", kTodo); + + // "one.two" + testWordCount("div8", 2, kOk); + testWordAt("div8", 0, "one", kTodo); + testWordAt("div8", 1, "two", kOk); + + // "345" + testWords("div9", ["345"]); + + // "3a A4" + testWords("div10", ["3a", "A4"]); + + // "3.1416" + testWords("div11", ["3.1416"], kTodo); + + // "4,261.01" + testWords("div12", ["4,261.01"], kTodo); + + // "カタカナ" + testWords("div13", ["カタカナ"], kOk); + + // "Peter's car" + testWords("div14", ["Peter's", "car"], kTodo); + + // "N.A.T.O." + testWords("div15", ["N.A.T.O."], kTodo); + + // "3+4*5=23" + testWordCount("div16", 4, kOk); + testWordAt("div15", 0, "3", kTodo); + testWordAt("div15", 1, "4", kTodo); + testWordAt("div15", 2, "5", kTodo); + testWordAt("div15", 3, "23", kTodo); + + // "Hello. Friend, are you here?!" + testWordCount("div17", 5, kOk); + testWordAt("div17", 0, "Hello", kTodo); + testWordAt("div17", 1, "Friend", kTodo); + testWordAt("div17", 2, "are", kOk); + testWordAt("div17", 3, "you", kOk); + testWordAt("div17", 4, "here", kTodo); + + testWords("input_1", ["foo", "bar"]); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="nsIAccessibleText test word boundaries" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=452769">Mozilla Bug 452769</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + <div id="div1">one two</div> + <div id="div2">one two</div> + <div id="div3">one,two</div> + <div id="div4">one, two</div> + <div id="div5">one+two</div> + <div id="div6">one+two </div> + <div id="div7">one<br/>two</div> + <div id="div8">one.two</div> + <div id="div9">345</div> + <div id="div10">3a A4</div> + <div id="div11">3.1416</div> + <div id="div12">4,261.01</div> + <div id="div13">カタカナ</div> + <div id="div14">Peter's car</div> + <div id="div15">N.A.T.O.</div> + <div id="div16">3+4*5=23</div> + <div id="div17">Hello. Friend, are you here?!</div> + </pre> + <input id="input_1" type="text" value="foo bar" placeholder="something or other"> + +</body> +</html> diff --git a/accessible/tests/mochitest/textattrs/a11y.ini b/accessible/tests/mochitest/textattrs/a11y.ini new file mode 100644 index 0000000000..98855d84b5 --- /dev/null +++ b/accessible/tests/mochitest/textattrs/a11y.ini @@ -0,0 +1,8 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/moz.png + +[test_general.html] +[test_invalid.html] +[test_spelling.html] diff --git a/accessible/tests/mochitest/textattrs/test_general.html b/accessible/tests/mochitest/textattrs/test_general.html new file mode 100644 index 0000000000..fe2b4a6dd3 --- /dev/null +++ b/accessible/tests/mochitest/textattrs/test_general.html @@ -0,0 +1,737 @@ +<html> + +<head> + <title>Text attributes tests</title> + <meta charset="utf-8" /> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style type="text/css"> + .gencontent:before { content: "*"; } + .gencontent:after { content: "*"; } + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + var gComputedStyle = null; + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // area1 + var ID = "area1"; + var defAttrs = buildDefaultTextAttrs(ID, "10pt"); + testDefaultTextAttrs(ID, defAttrs); + + var attrs = {}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 7); + + attrs = { "font-weight": kBoldFontWeight }; + testTextAttrs(ID, 7, attrs, defAttrs, 7, 11); + + attrs = {}; + testTextAttrs(ID, 12, attrs, defAttrs, 11, 18); + + // //////////////////////////////////////////////////////////////////////// + // area2 + ID = "area2"; + defAttrs = buildDefaultTextAttrs(ID, "14pt"); + testDefaultTextAttrs(ID, defAttrs); + + attrs = {}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 7); + + attrs = { "font-weight": kBoldFontWeight }; + testTextAttrs(ID, 7, attrs, defAttrs, 7, 12); + + var tempElem = getNode(ID).firstChild.nextSibling.firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"font-style": gComputedStyle.fontStyle, + "font-weight": kBoldFontWeight }; + testTextAttrs(ID, 13, attrs, defAttrs, 12, 19); + + attrs = { "font-weight": kBoldFontWeight }; + testTextAttrs(ID, 20, attrs, defAttrs, 19, 23); + + attrs = {}; + testTextAttrs(ID, 24, attrs, defAttrs, 23, 30); + + // //////////////////////////////////////////////////////////////////////// + // area3 + ID = "area3"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + testDefaultTextAttrs(ID, defAttrs); + + tempElem = getNode(ID).firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 6); + + tempElem = tempElem.firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 6, attrs, defAttrs, 6, 26); + + tempElem = tempElem.parentNode; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 26, attrs, defAttrs, 26, 27); + + tempElem = tempElem.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color, + "background-color": gComputedStyle.backgroundColor}; + testTextAttrs(ID, 27, attrs, defAttrs, 27, 50); + + // //////////////////////////////////////////////////////////////////////// + // area4 + ID = "area4"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + testDefaultTextAttrs(ID, defAttrs); + + tempElem = getNode(ID).firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 16); + + tempElem = tempElem.nextSibling.firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 16, attrs, defAttrs, 16, 33); + + tempElem = tempElem.parentNode; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 34, attrs, defAttrs, 33, 46); + + // //////////////////////////////////////////////////////////////////////// + // area5: "Green!*!RedNormal" + ID = "area5"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + testDefaultTextAttrs(ID, defAttrs); + + // Green + tempElem = getNode(ID).firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 5); + + // br + attrs = {}; + testTextAttrs(ID, 5, attrs, defAttrs, 5, 6); + + // img, embedded accessible, no attributes + attrs = {}; + testTextAttrs(ID, 6, attrs, {}, 6, 7); + + // br + attrs = {}; + testTextAttrs(ID, 7, attrs, defAttrs, 7, 8); + + // Red + tempElem = tempElem.nextSibling.nextSibling.nextSibling.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 9, attrs, defAttrs, 8, 11); + + // Normal + attrs = {}; + testTextAttrs(ID, 11, attrs, defAttrs, 11, 18); + + // //////////////////////////////////////////////////////////////////////// + // area6 (CSS vertical-align property, refer to bug 445938 for details + // and sup and sub elements, refer to bug 735645 for details) + ID = "area6"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + testDefaultTextAttrs(ID, defAttrs); + + attrs = {}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 5); + + attrs = { "text-position": "super", "font-size": "10pt" }; + testTextAttrs(ID, 5, attrs, defAttrs, 5, 13); + + attrs = {}; + testTextAttrs(ID, 13, attrs, defAttrs, 13, 27); + + attrs = { "text-position": "super" }; + testTextAttrs(ID, 27, attrs, defAttrs, 27, 35); + + attrs = {}; + testTextAttrs(ID, 35, attrs, defAttrs, 35, 39); + + attrs = { "text-position": "sub", "font-size": "10pt" }; + testTextAttrs(ID, 39, attrs, defAttrs, 39, 50); + + attrs = {}; + testTextAttrs(ID, 50, attrs, defAttrs, 50, 55); + + attrs = { "text-position": "sub" }; + testTextAttrs(ID, 55, attrs, defAttrs, 55, 64); + + attrs = {}; + testTextAttrs(ID, 64, attrs, defAttrs, 64, 69); + + attrs = { "text-position": "super" }; + testTextAttrs(ID, 69, attrs, defAttrs, 69, 84); + + attrs = {}; + testTextAttrs(ID, 84, attrs, defAttrs, 84, 89); + + attrs = { "text-position": "sub" }; + testTextAttrs(ID, 89, attrs, defAttrs, 89, 102); + + attrs = {}; + testTextAttrs(ID, 102, attrs, defAttrs, 102, 107); + + attrs = { "text-position": "super" }; + testTextAttrs(ID, 107, attrs, defAttrs, 107, 123); + + attrs = {}; + testTextAttrs(ID, 123, attrs, defAttrs, 123, 128); + + attrs = { "text-position": "sub" }; + testTextAttrs(ID, 128, attrs, defAttrs, 128, 142); + + // //////////////////////////////////////////////////////////////////////// + // area7 + ID = "area7"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + defAttrs.language = "en"; + testDefaultTextAttrs(ID, defAttrs); + + attrs = {"language": "ru"}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 6); + + attrs = {}; + testTextAttrs(ID, 6, attrs, defAttrs, 6, 7); + + tempElem = getNode(ID).firstChild.nextSibling.nextSibling.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = { "background-color": gComputedStyle.backgroundColor}; + testTextAttrs(ID, 13, attrs, defAttrs, 7, 20); + + attrs = {}; + testTextAttrs(ID, 20, attrs, defAttrs, 20, 21); + + attrs = {"language": "de"}; + testTextAttrs(ID, 21, attrs, defAttrs, 21, 36); + + attrs = {}; + testTextAttrs(ID, 36, attrs, defAttrs, 36, 44); + + attrs = {}; + testTextAttrs(ID, 37, attrs, defAttrs, 36, 44); + + tempElem = tempElem.nextSibling.nextSibling.nextSibling.nextSibling.firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 44, attrs, defAttrs, 44, 51); + + tempElem = tempElem.firstChild.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"font-weight": kBoldFontWeight, + "color": gComputedStyle.color}; + testTextAttrs(ID, 51, attrs, defAttrs, 51, 55); + + tempElem = tempElem.parentNode; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"color": gComputedStyle.color}; + testTextAttrs(ID, 55, attrs, defAttrs, 55, 62); + + // //////////////////////////////////////////////////////////////////////// + // area9, different single style spans in styled paragraph + ID = "area9"; + defAttrs = buildDefaultTextAttrs(ID, "10pt"); + testDefaultTextAttrs(ID, defAttrs); + + attrs = {}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 6); + + attrs = { "font-size": "12pt" }; + testTextAttrs(ID, 7, attrs, defAttrs, 6, 12); + + attrs = {}; + testTextAttrs(ID, 13, attrs, defAttrs, 12, 21); + + // Walk to the span with the different background color + tempElem = getNode(ID).firstChild.nextSibling.nextSibling.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = { "background-color": gComputedStyle.backgroundColor }; + testTextAttrs(ID, 22, attrs, defAttrs, 21, 36); + + attrs = {}; + testTextAttrs(ID, 37, attrs, defAttrs, 36, 44); + + // Walk from the background color span to the one with font-style + tempElem = tempElem.nextSibling.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = { "font-style": gComputedStyle.fontStyle }; + testTextAttrs(ID, 45, attrs, defAttrs, 44, 61); + + attrs = {}; + testTextAttrs(ID, 62, attrs, defAttrs, 61, 69); + + // Walk from span with font-style to the one with font-family. + tempElem = tempElem.nextSibling.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = { "font-family": kMonospaceFontFamily }; + testTextAttrs(ID, 70, attrs, defAttrs, 69, 83); + + attrs = {}; + testTextAttrs(ID, 84, attrs, defAttrs, 83, 91); + + attrs = { + "text-underline-style": "solid", + "text-underline-color": gComputedStyle.color, + }; + testTextAttrs(ID, 92, attrs, defAttrs, 91, 101); + + attrs = {}; + testTextAttrs(ID, 102, attrs, defAttrs, 101, 109); + + attrs = { + "text-line-through-style": "solid", + "text-line-through-color": gComputedStyle.color, + }; + testTextAttrs(ID, 110, attrs, defAttrs, 109, 122); + + attrs = {}; + testTextAttrs(ID, 123, attrs, defAttrs, 122, 130); + + attrs = { + "text-line-through-style": "solid", + "text-line-through-color": gComputedStyle.color, + }; + testTextAttrs(ID, 131, attrs, defAttrs, 130, 143); + + attrs = {}; + testTextAttrs(ID, 144, attrs, defAttrs, 143, 151); + + attrs = { + "text-line-through-style": "solid", + "text-line-through-color": gComputedStyle.color, + }; + testTextAttrs(ID, 152, attrs, defAttrs, 151, 164); + + attrs = {}; + testTextAttrs(ID, 165, attrs, defAttrs, 164, 172); + + // //////////////////////////////////////////////////////////////////////// + // area10, different single style spans in non-styled paragraph + ID = "area10"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + testDefaultTextAttrs(ID, defAttrs); + + attrs = {}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 7); + + attrs = { "font-size": "14pt" }; + testTextAttrs(ID, 7, attrs, defAttrs, 7, 13); + + attrs = {}; + testTextAttrs(ID, 13, attrs, defAttrs, 13, 22); + + // Walk to the span with the different background color + tempElem = getNode(ID).firstChild.nextSibling.nextSibling.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = { "background-color": gComputedStyle.backgroundColor }; + testTextAttrs(ID, 23, attrs, defAttrs, 22, 37); + + attrs = {}; + testTextAttrs(ID, 38, attrs, defAttrs, 37, 45); + + // Walk from the background color span to the one with font-style + tempElem = tempElem.nextSibling.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = {"font-style": gComputedStyle.fontStyle}; + testTextAttrs(ID, 46, attrs, defAttrs, 45, 62); + + attrs = {}; + testTextAttrs(ID, 63, attrs, defAttrs, 62, 70); + + // Walk from span with font-style to the one with font-family. + tempElem = tempElem.nextSibling.nextSibling; + gComputedStyle = document.defaultView.getComputedStyle(tempElem); + attrs = { "font-family": kMonospaceFontFamily }; + testTextAttrs(ID, 71, attrs, defAttrs, 70, 84); + + attrs = {}; + testTextAttrs(ID, 85, attrs, defAttrs, 84, 92); + + attrs = { + "text-underline-style": "solid", + "text-underline-color": gComputedStyle.color, + }; + testTextAttrs(ID, 93, attrs, defAttrs, 92, 102); + + attrs = {}; + testTextAttrs(ID, 103, attrs, defAttrs, 102, 110); + + attrs = { + "text-line-through-style": "solid", + "text-line-through-color": gComputedStyle.color, + }; + testTextAttrs(ID, 111, attrs, defAttrs, 110, 123); + + attrs = {}; + testTextAttrs(ID, 124, attrs, defAttrs, 123, 131); + + // //////////////////////////////////////////////////////////////////////// + // area11, "font-weight" tests + ID = "area11"; + defAttrs = buildDefaultTextAttrs(ID, "12pt", kBoldFontWeight); + testDefaultTextAttrs(ID, defAttrs); + + attrs = { }; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 13); + + attrs = { "font-weight": kNormalFontWeight }; + testTextAttrs(ID, 13, attrs, defAttrs, 13, 20); + + attrs = { }; + testTextAttrs(ID, 20, attrs, defAttrs, 20, 27); + + attrs = { "font-weight": kNormalFontWeight }; + testTextAttrs(ID, 27, attrs, defAttrs, 27, 33); + + attrs = { }; + testTextAttrs(ID, 33, attrs, defAttrs, 33, 51); + + attrs = { "font-weight": kNormalFontWeight }; + testTextAttrs(ID, 51, attrs, defAttrs, 51, 57); + + attrs = { }; + testTextAttrs(ID, 57, attrs, defAttrs, 57, 97); + + // //////////////////////////////////////////////////////////////////////// + // test out of range offset + testTextAttrsWrongOffset("area12", -1); + testTextAttrsWrongOffset("area12", 500); + + // //////////////////////////////////////////////////////////////////////// + // test zero offset on empty hypertext accessibles + ID = "area13"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + attrs = { }; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 0); + + ID = "area14"; + defAttrs = buildDefaultTextAttrs(ID, kInputFontSize, + kNormalFontWeight, kInputFontFamily); + + attrs = { }; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 0); + + // //////////////////////////////////////////////////////////////////////// + // area15, embed char tests, "*plain*plain**bold*bold*" + ID = "area15"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + + // p + testTextAttrs(ID, 0, { }, { }, 0, 1); + // plain + testTextAttrs(ID, 1, { }, defAttrs, 1, 6); + // p + testTextAttrs(ID, 6, { }, { }, 6, 7); + // plain + testTextAttrs(ID, 7, { }, defAttrs, 7, 12); + // p and img + testTextAttrs(ID, 12, { }, { }, 12, 14); + // bold + attrs = { "font-weight": kBoldFontWeight }; + testTextAttrs(ID, 14, attrs, defAttrs, 14, 18); + // p + testTextAttrs(ID, 18, { }, { }, 18, 19); + // bold + attrs = { "font-weight": kBoldFontWeight }; + testTextAttrs(ID, 19, attrs, defAttrs, 19, 23); + // p + testTextAttrs(ID, 23, { }, { }, 23, 24); + + // //////////////////////////////////////////////////////////////////////// + // area16, "font-family" tests + ID = "area16"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + testDefaultTextAttrs(ID, defAttrs); + + attrs = { "font-family": kMonospaceFontFamily }; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 4); + + attrs = { }; + testTextAttrs(ID, 4, attrs, defAttrs, 4, 9); + + attrs = { "font-family": kSerifFontFamily }; + testTextAttrs(ID, 9, attrs, defAttrs, 9, 13); + + attrs = { }; + testTextAttrs(ID, 13, attrs, defAttrs, 13, 18); + + attrs = { "font-family": kAbsentFontFamily }; + testTextAttrs(ID, 18, attrs, defAttrs, 18, 22); + + // bug 1224498 - this fails with 'cursive' fontconfig lookup + if (!LINUX) { + attrs = { }; + testTextAttrs(ID, 22, attrs, defAttrs, 22, 27); + + attrs = { "font-family": kCursiveFontFamily }; + testTextAttrs(ID, 27, attrs, defAttrs, 27, 31); + + attrs = { }; + testTextAttrs(ID, 31, attrs, defAttrs, 31, 45); + } + + // //////////////////////////////////////////////////////////////////////// + // area17, "text-decoration" tests + ID = "area17"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + testDefaultTextAttrs(ID, defAttrs); + + attrs = { + "text-underline-style": "solid", + "text-underline-color": "rgb(0, 0, 0)", + }; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 10); + + attrs = { + "text-underline-style": "solid", + "text-underline-color": "rgb(0, 0, 255)", + }; + testTextAttrs(ID, 10, attrs, defAttrs, 10, 15); + + attrs = { + "text-underline-style": "dotted", + "text-underline-color": "rgb(0, 0, 0)", + }; + testTextAttrs(ID, 15, attrs, defAttrs, 15, 22); + + attrs = { + "text-line-through-style": "solid", + "text-line-through-color": "rgb(0, 0, 0)", + }; + testTextAttrs(ID, 22, attrs, defAttrs, 22, 34); + + attrs = { + "text-line-through-style": "solid", + "text-line-through-color": "rgb(0, 0, 255)", + }; + testTextAttrs(ID, 34, attrs, defAttrs, 34, 39); + + attrs = { + "text-line-through-style": "wavy", + "text-line-through-color": "rgb(0, 0, 0)", + }; + testTextAttrs(ID, 39, attrs, defAttrs, 39, 44); + + // //////////////////////////////////////////////////////////////////////// + // area18, "auto-generation text" tests + ID = "area18"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + testDefaultTextAttrs(ID, defAttrs); + + attrs = { + "auto-generated": "true", + }; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 3); + testTextAttrs(ID, 3, { }, defAttrs, 3, 7); + testTextAttrs(ID, 7, attrs, defAttrs, 7, 8); + + // //////////////////////////////////////////////////////////////////////// + // area19, "HTML5 mark tag" test + // text enclosed in mark tag will have a different background color + // However, since bug 982125, it is its own accessible. + // Therefore, anything other than the default background color is + // unexpected. + ID = "area19"; + defAttrs = buildDefaultTextAttrs(ID, "12pt"); + + attrs = {}; + testTextAttrs(ID, 0, attrs, defAttrs, 0, 10); + + ID = "area19mark"; + let defMarkAttrs = buildDefaultTextAttrs(ID, "12pt"); + attrs = {}; + testTextAttrs(ID, 0, attrs, defMarkAttrs, 0, 7); + + ID = "area19"; + attrs = {}; + testTextAttrs(ID, 11, attrs, defAttrs, 11, 22); + + // //////////////////////////////////////////////////////////////////////// + // area20, "aOffset as -1 (Mozilla Bug 789621)" test + + ID = "area20"; + defAttrs = buildDefaultTextAttrs(ID, "15pt"); + testDefaultTextAttrs(ID, defAttrs); + + testTextAttrs(ID, -1, {}, defAttrs, 0, 11); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body style="font-size: 12pt"> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=345759" + title="Implement text attributes"> + Mozilla Bug 345759 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=473569" + title="Restrict text-position to allowed values"> + Mozilla Bug 473569 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=473576" + title="font-family text attribute should expose actual font used"> + Mozilla Bug 473576 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=523304" + title="expose text-underline-color and text-line-through-color text attributes"> + Mozilla Bug 523304 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=735645" + title="expose sub and sup elements in text attributes"> + Mozilla Bug 735645 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=445516" + title="Support auto-generated text attribute on bullet lists"> + Mozilla Bug 445516 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=789621" + title="getTextAttributes doesn't work with magic offsets"> + Mozilla Bug 789621 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="area1" style="font-size: smaller">Normal <b>Bold</b> Normal</p> + <p id="area2" style="font-size: 120%">Normal <b>Bold <i>Italic </i>Bold</b> Normal</p> + <p id="area3" style="background-color: blue;"> + <span style="color: green; background-color: rgb(0, 0, 255)"> + Green + <span style="color: red">but children are red</span> + </span><span style="color: green; background-color: rgb(255, 255, 0);"> + Another green section. + </span> + </p> + <p id="area4"> + <span style="color: green"> + Green + </span><span style="color: green"> + Green too + <span style="color: red">with red children</span> + Green again + </span> + </p> + <!-- Green!*!RedNormal--> + <p id="area5"> + <span style="color: green">Green</span> + <img src="../moz.png" alt="image"/> + <span style="color: red">Red</span>Normal + </p> + <p id="area6"> + This <sup>sentence</sup> has the word + <span style="vertical-align:super;">sentence</span> in + <sub>superscript</sub> and + <span style="vertical-align:sub;">subscript</span> and + <span style="vertical-align:20%;">superscript 20%</span> and + <span style="vertical-align:-20%;">subscript 20%</span> and + <span style="vertical-align:20px;">superscript 20px</span> and + <span style="vertical-align:-20px;">subscript 20px</span> + </p> + + <p lang="en" id="area7"> + <span lang="ru">Привет</span> + <span style="background-color: blue">Blue BG color</span> + <span lang="de">Ich bin/Du bist</span> + <span lang="en"> + Normal + <span style="color: magenta">Magenta<b>Bold</b>Magenta</span> + </span> + </p> + + <p id="area9" style="font-size: smaller">Small + <span style="font-size: 120%">bigger</span> smaller + <span style="background-color: blue;">background blue</span> normal + <span style="font-style: italic;">Different styling</span> normal + <span style="font-family: monospace;">Different font</span> normal + <span style="text-decoration: underline;">underlined</span> normal + <span style="text-decoration: line-through;">strikethrough</span> normal + <s>strikethrough</s> normal + <strike>strikethrough</strike> normal + </p> + + <p id="area10">Normal + <span style="font-size: 120%">bigger</span> smaller + <span style="background-color: blue;">background blue</span> normal + <span style="font-style: italic;">Different styling</span> normal + <span style="font-family: monospace;">Different font</span> normal + <span style="text-decoration: underline;">underlined</span> normal + <span style="text-decoration: line-through;">strikethrough</span> normal + </p> + + <p id="area11" style="font-weight: bolder;"> + <span style="font-weight: bolder;">bolder</span>bolder + <span style="font-weight: lighter;">lighter</span>bolder + <span style="font-weight: normal;">normal</span>bolder + <b>bold</b>bolder + <span style="font-weight: 400;">normal</span>bolder + <span style="font-weight: 700;">bold</span>bolder + <span style="font-weight: bold;">bold</span>bolder + <span style="font-weight: 900;">bold</span>bolder + </p> + + <p id="area12">hello</p> + <p id="area13"></p> + <input id="area14"> + + <!-- *plain*plain**bold*bold*--> + <div id="area15"><p>embed</p>plain<p>embed</p>plain<p>embed</p><img src="../moz.png" alt="image"/><b>bold</b><p>embed</p><b>bold</b><p>embed</p></div> + + <p id="area16" style="font-family: sans-serif;"> + <span style="font-family: monospace;">text</span>text + <span style="font-family: serif;">text</span>text + <span style="font-family: BodoniThatDoesntExist;">text</span>text + <span style="font-family: Comic Sans MS, cursive;">text</span>text + <span style="font-family: sans-serif, fantasy;">text</span>text + </p> + + <p id="area17"> + <span style="text-decoration-line: underline;">underline + </span><span style="text-decoration: underline; text-decoration-color: blue;">blue + </span><span style="text-decoration: underline; text-decoration-style: dotted;">dotted + </span><span style="text-decoration-line: line-through;">linethrough + </span><span style="text-decoration: line-through; text-decoration-color: blue;">blue + </span><span style="text-decoration: line-through; text-decoration-style: wavy;">wavy + </span> + </p> + + <ul> + <li id="area18" class="gencontent">item</li> + </ul> + + <p id="area19">uncolored + <mark id="area19mark">colored</mark> uncolored + </p> + + <p id="area20" style="font-size: 15pt;">offset test</p> + +</body> +</html> diff --git a/accessible/tests/mochitest/textattrs/test_invalid.html b/accessible/tests/mochitest/textattrs/test_invalid.html new file mode 100644 index 0000000000..7028b32622 --- /dev/null +++ b/accessible/tests/mochitest/textattrs/test_invalid.html @@ -0,0 +1,59 @@ +<html> + +<head> + <title>Invalid text attribute</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + + <script type="application/javascript"> + function doTests() { + testDefaultTextAttrs("aria_invalid_empty", {}, true); + testDefaultTextAttrs("aria_invalid_true", { "invalid": "true" }, true); + testDefaultTextAttrs("aria_invalid_false", { "invalid": "false" }, true); + testDefaultTextAttrs("aria_invalid_grammar", { "invalid": "grammar" }, true); + testDefaultTextAttrs("aria_invalid_spelling", { "invalid": "spelling" }, true); + testDefaultTextAttrs("aria_invalid_erroneous", { "invalid": "true" }, true); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=445510" + title="Support ARIA-based text attributes"> + Mozilla Bug 445510 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="aria_invalid_empty" aria-invalid="">no invalid</div> + <div id="aria_invalid_true" aria-invalid="true">invalid:true</div> + <div id="aria_invalid_false" aria-invalid="false">invalid:false</div> + <div id="aria_invalid_grammar" aria-invalid="grammar">invalid:grammar</div> + <div id="aria_invalid_spelling" aria-invalid="spelling">invalid:spelling</div> + <div id="aria_invalid_erroneous" aria-invalid="erroneous">invalid:true</div> +</body> +</html> diff --git a/accessible/tests/mochitest/textattrs/test_spelling.html b/accessible/tests/mochitest/textattrs/test_spelling.html new file mode 100644 index 0000000000..b8f3858353 --- /dev/null +++ b/accessible/tests/mochitest/textattrs/test_spelling.html @@ -0,0 +1,52 @@ +<html> + +<head> + <title>Spell check text attribute tests</title> + <meta charset="utf-8" /> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../attributes.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function doTest() { + const misspelledAttrs = {"invalid": "spelling"}; + + let editable = document.getElementById("div_after_misspelling"); + // The attr change event gets fired on the last accessible containing a + // spelling error. + let spellDone = waitForEvent(EVENT_TEXT_ATTRIBUTE_CHANGED, "div_after_misspelling_div2"); + editable.focus(); + await spellDone; + testTextAttrs("div_after_misspelling_div1", 0, {}, {}, 0, 5, true); + testTextAttrs("div_after_misspelling_div1", 5, misspelledAttrs, {}, 5, 9, true); + testTextAttrs("div_after_misspelling_div2", 0, {}, {}, 0, 5, true); + testTextAttrs("div_after_misspelling_div2", 5, misspelledAttrs, {}, 5, 9, true); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Text attribute offsets for accessibles after first spelling error (bug 1479678) --> + <div id="div_after_misspelling" contenteditable="true" spellcheck="true" lang="en-US"> + <div id="div_after_misspelling_div1">Test tset</div> + <div id="div_after_misspelling_div2">test tset</div> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/textcaret/a11y.ini b/accessible/tests/mochitest/textcaret/a11y.ini new file mode 100644 index 0000000000..6e2e4ef8d7 --- /dev/null +++ b/accessible/tests/mochitest/textcaret/a11y.ini @@ -0,0 +1,5 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_general.html] diff --git a/accessible/tests/mochitest/textcaret/test_general.html b/accessible/tests/mochitest/textcaret/test_general.html new file mode 100644 index 0000000000..b40f1a982c --- /dev/null +++ b/accessible/tests/mochitest/textcaret/test_general.html @@ -0,0 +1,174 @@ +<html> + +<head> + <title>Text accessible caret testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Turn on/off the caret browsing mode. + */ + function turnCaretBrowsing(aIsOn) { + Services.prefs.setBoolPref("accessibility.browsewithcaret", aIsOn); + } + + /** + * Test caret offset for the given accessible. + */ + function testCaretOffset(aID, aCaretOffset) { + var acc = getAccessible(aID, [nsIAccessibleText]); + is(acc.caretOffset, aCaretOffset, + "Wrong caret offset for " + aID); + } + + function testCaretOffsets(aList) { + for (var i = 0; i < aList.length; i++) + testCaretOffset(aList[0][0], aList[0][1]); + } + + function queueTraversalList(aList, aFocusNode) { + for (var i = 0 ; i < aList.length; i++) { + var node = aList[i].DOMPoint[0]; + var nodeOffset = aList[i].DOMPoint[1]; + + var textAcc = aList[i].point[0]; + var textOffset = aList[i].point[1]; + var textList = aList[i].pointList; + var invoker = + new moveCaretToDOMPoint(textAcc, node, nodeOffset, textOffset, + ((i == 0) ? aFocusNode : null), + testCaretOffsets.bind(null, textList)); + gQueue.push(invoker); + } + } + + /** + * Do tests. + */ + var gQueue = null; + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function doTests() { + turnCaretBrowsing(true); + + // test caret offsets + testCaretOffset(document, 15); + testCaretOffset("textbox", -1); + testCaretOffset("textarea", -1); + testCaretOffset("p", -1); + + // test caret move events and caret offsets + gQueue = new eventQueue(); + + gQueue.push(new setCaretOffset("textbox", 1, "textbox")); + gQueue.push(new setCaretOffset("link", 1, "link")); + gQueue.push(new setCaretOffset("heading", 1, document)); + + // a*b*c + var p2Doc = getNode("p2_container").contentDocument; + var traversalList = [ + { // before 'a' + DOMPoint: [ getNode("p2", p2Doc).firstChild, 0 ], + point: [ getNode("p2", p2Doc), 0 ], + pointList: [ [ p2Doc, 0 ] ], + }, + { // after 'a' (before anchor) + DOMPoint: [ getNode("p2", p2Doc).firstChild, 1 ], + point: [ getNode("p2", p2Doc), 1 ], + pointList: [ [ p2Doc, 0 ] ], + }, + { // before 'b' (inside anchor) + DOMPoint: [ getNode("p2_a", p2Doc).firstChild, 0 ], + point: [ getNode("p2_a", p2Doc), 0 ], + pointList: [ + [ getNode("p2", p2Doc), 1 ], + [ p2Doc, 0 ], + ], + }, + { // after 'b' (inside anchor) + DOMPoint: [ getNode("p2_a", p2Doc).firstChild, 1 ], + point: [ getNode("p2_a", p2Doc), 1 ], + pointList: [ + [ getNode("p2", p2Doc), 1 ], + [ p2Doc, 0 ], + ], + }, + { // before 'c' (after anchor) + DOMPoint: [ getNode("p2", p2Doc).lastChild, 0 ], + point: [ getNode("p2", p2Doc), 2 ], + pointList: [ [ p2Doc, 0 ] ], + }, + { // after 'c' + DOMPoint: [ getNode("p2", p2Doc).lastChild, 1 ], + point: [ getNode("p2", p2Doc), 3 ], + pointList: [ [ p2Doc, 0 ] ], + }, + ]; + queueTraversalList(traversalList, getNode("p2", p2Doc)); + + gQueue.onFinish = function() { + turnCaretBrowsing(false); + }; + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=448744" + title="caretOffset should return -1 if the system caret is not currently with in that particular object"> + Bug 448744 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=524115" + title="HyperText accessible should get focus when the caret is positioned inside of it, text is changed or copied into clipboard by ATs"> + Bug 524115 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=546068" + title="Position is not being updated when atk_text_set_caret_offset is used"> + Bug 546068 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=672717" + title="Broken caret when moving into/out of embedded objects with right arrow"> + Bug 672717 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=725581" + title="caretOffset for textarea should be -1 when textarea doesn't have a focus"> + Bug 725581 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="textbox" value="hello"/> + <textarea id="textarea">text<br>text</textarea> + <p id="p" contentEditable="true"><span>text</span><br/>text</p> + <a id="link" href="about:mozilla">about mozilla</a> + <h5 id="heading">heading</h5> + <iframe id="p2_container" + src="data:text/html,<p id='p2' contentEditable='true'>a<a id='p2_a' href='mozilla.org'>b</a>c</p>"></iframe> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/textrange/a11y.ini b/accessible/tests/mochitest/textrange/a11y.ini new file mode 100644 index 0000000000..4e610c61f3 --- /dev/null +++ b/accessible/tests/mochitest/textrange/a11y.ini @@ -0,0 +1,7 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/moz.png + +[test_general.html] +[test_selection.html] diff --git a/accessible/tests/mochitest/textrange/test_general.html b/accessible/tests/mochitest/textrange/test_general.html new file mode 100644 index 0000000000..b7da4ee8aa --- /dev/null +++ b/accessible/tests/mochitest/textrange/test_general.html @@ -0,0 +1,106 @@ +<!DOCTYPE html> +<html> +<head> + <title>Text Range tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + <script type="application/javascript"> + + function doTest() { + // enclosingRange + var input = getAccessible("input", [ nsIAccessibleText ]); + testTextRange(input.enclosingRange, "enclosing range for 'input'", + input, 0, input, 5, "hello", input); + + var ta = getAccessible("textarea", [ nsIAccessibleText ]); + testTextRange(ta.enclosingRange, "enclosing range for 'textarea'", + ta, 0, ta, 5, "hello", ta); + + var iframeDocNode = getNode("iframe").contentDocument; + var iframeDoc = getAccessible(iframeDocNode, [ nsIAccessibleText ]); + testTextRange(iframeDoc.enclosingRange, "enclosing range for iframe doc", + iframeDoc, 0, iframeDoc, 1, "hello", + iframeDoc, [ getNode("p", iframeDocNode) ]); + + // getRangeByChild + var docacc = getAccessible(document, [ nsIAccessibleText ]); + var p1 = getAccessible("p1"); + var p1Range = docacc.getRangeByChild(p1); + testTextRange(p1Range, "range by 'p1' child", + p1, 0, "p1", 11, "text text", + p1, ["p1_img"]); + + testTextRange(docacc.getRangeByChild(getAccessible("p1_img")), + "range by 'p1_img' child", + "p1", 5, "p1", 5, "", + "p1", ["p1_img"]); + + var p2 = getAccessible("p2"); + var p2Range = docacc.getRangeByChild(p2); + testTextRange(p2Range, "range by 'p2' child", + p2, 0, "p2", 11, "text link text", + p2, ["p2_a"]); + + testTextRange(docacc.getRangeByChild(getAccessible("p2_a")), + "range by 'p2_a' child", + "p2_a", 0, "p2_a", 5, "link", + "p2_a", ["p2_img"]); + + // getRangeAtPoint + getNode("p2_a").scrollIntoView(true); + var [x, y] = getPos("p2_a"); + testTextRange(docacc.getRangeAtPoint(x + 1, y + 1), + "range at 'p2_a' top-left edge", + "p2_a", 0, "p2_a", 0, "", + "p2_a"); + + // TextRange::compare + ok(input.enclosingRange.compare(input.enclosingRange), + "input enclosing ranges should be equal"); + + ok(!input.enclosingRange.compare(ta.enclosingRange), + "input and textarea enclosing ranges can't be equal"); + + // TextRange::compareEndPoints + var res = p1Range.compareEndPoints(EndPoint_End, p2Range, EndPoint_Start); + is(res, -1, "p1 range must be lesser with p2 range"); + + res = p2Range.compareEndPoints(EndPoint_Start, p1Range, EndPoint_End); + is(res, 1, "p2 range must be greater with p1 range"); + + res = p1Range.compareEndPoints(EndPoint_Start, p1Range, EndPoint_Start); + is(res, 0, "p1 range must be equal with p1 range"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Implement Text accessible text range methods" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=975065">Bug 975065</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input id="input" value="hello"> + <textarea id="textarea">hello</textarea> + <iframe id="iframe" src="data:text/html,<html><body><p id='p'>hello</p></body></html>"></iframe> + <p id="p1">text <img id="p1_img", src="../moz.png"> text</p> + <p id="p2">text <a id="p2_a" href="www">link<img id="p2_img", src="../moz.png"></a> text</p> + +</body> +</html> diff --git a/accessible/tests/mochitest/textrange/test_selection.html b/accessible/tests/mochitest/textrange/test_selection.html new file mode 100644 index 0000000000..fb1c0884df --- /dev/null +++ b/accessible/tests/mochitest/textrange/test_selection.html @@ -0,0 +1,129 @@ +<!DOCTYPE html> +<html> +<head> + <title>Text Range selection tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../text.js"></script> + <script type="application/javascript" + src="../layout.js"></script> + <script type="application/javascript"> + + function doTest() { + var sel = window.getSelection(); + var p = getNode("p1"); + var a = getNode("p2_a"); + + var range = document.createRange(); + sel.addRange(range); + + // the accessible is contained by the range + range.selectNode(p); + + var a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges; + var a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange); + + testTextRange(a11yrange, "selection range #1", document, 3, document, 4); + + ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #1."); + testTextRange(a11yrange, "cropped range #1", a, 0, a, 5); + + // the range is contained by the accessible + range.selectNode(a); + a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges; + a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange); + + testTextRange(a11yrange, "selection range #2", p, 5, p, 6); + + ok(a11yrange.crop(getAccessible(p)), "Range failed to crop #2."); + testTextRange(a11yrange, "cropped range #2", p, 5, p, 6); + + // the range starts before the accessible and ends inside it + range.setStart(p, 0); + range.setEndAfter(a.firstChild, 4); + a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges; + a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange); + + testTextRange(a11yrange, "selection range #3", p, 0, a, 4); + + ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #3."); + testTextRange(a11yrange, "cropped range #3", a, 0, a, 4); + + // the range starts inside the accessible and ends after it + range.setStart(a.firstChild, 1); + range.setEndAfter(p); + a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges; + a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange); + + testTextRange(a11yrange, "selection range #4", a, 1, document, 4); + + ok(a11yrange.crop(getAccessible(a)), "Range failed to crop #4."); + testTextRange(a11yrange, "cropped range #4", a, 1, a, 5); + + // the range ends before the accessible + range.setStart(p.firstChild, 0); + range.setEnd(p.firstChild, 4); + a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges; + a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange); + + testTextRange(a11yrange, "selection range #5", p, 0, p, 4); + ok(!a11yrange.crop(getAccessible(a)), "Crop #5 succeeded while it shouldn't"); + + // the range starts after the accessible + range.setStart(p.lastChild, 0); + range.setEnd(p.lastChild, 4); + a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges; + a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange); + + testTextRange(a11yrange, "selection range #6", p, 6, p, 10); + + ok(!a11yrange.crop(getAccessible(a)), "Crop #6 succeeded while it shouldn't"); + + // crop a range by a table + range.selectNode(getNode("c2")); + a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges; + a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange); + + testTextRange(a11yrange, "selection range #7", document, 4, document, 5); + + ok(a11yrange.crop(getAccessible("table")), "Range failed to crop #7."); + testTextRange(a11yrange, "cropped range #7", "table", 0, "table", 1); + + // test compare points for selection with start in nested node + range.setStart(a.firstChild, 2); + range.setEnd(p.lastChild, 3); + a11yranges = getAccessible(document, [nsIAccessibleText]).selectionRanges; + a11yrange = a11yranges.queryElementAt(0, nsIAccessibleTextRange); + var res = a11yrange.compareEndPoints(EndPoint_Start, a11yrange, EndPoint_End); + is(res, -1, "start must be lesser than end"); + + res = a11yrange.compareEndPoints(EndPoint_End, a11yrange, EndPoint_Start); + is(res, 1, "end must be greater than start"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Implement IAccessible2_3::selectionRanges" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1233118">Bug 1233118</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="p1">text <a id="p2_a" href="www">link<img id="p2_img", src="../moz.png"></a> text</p> + + <div id="c2">start<table id="table"><tr><td>cell</td></tr></table>end</div> +</body> +</html> diff --git a/accessible/tests/mochitest/textselection/a11y.ini b/accessible/tests/mochitest/textselection/a11y.ini new file mode 100644 index 0000000000..6581af56db --- /dev/null +++ b/accessible/tests/mochitest/textselection/a11y.ini @@ -0,0 +1,6 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_general.html] +[test_userinput.html] diff --git a/accessible/tests/mochitest/textselection/test_general.html b/accessible/tests/mochitest/textselection/test_general.html new file mode 100644 index 0000000000..92e7988a87 --- /dev/null +++ b/accessible/tests/mochitest/textselection/test_general.html @@ -0,0 +1,221 @@ +<html> + +<head> + <title>Text selection testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + /** + * Helper function to test selection bounds. + * @param {string} aID The ID to test. + * @param {nsIAccessibleText} acc The accessible to test. + * @param {int} index The selection's index to test. + * @param {array} offsets The start and end offset to test against. + * @param {string} msgStart The start of the message to return in test + * messages. + */ + function testSelectionBounds(aID, acc, index, offsets, msgStart) { + const [expectedStart, expectedEnd] = offsets; + const startOffset = {}, endOffset = {}; + acc.getSelectionBounds(index, startOffset, endOffset); + + is(startOffset.value, Math.min(expectedStart, expectedEnd), + msgStart + ": Wrong start offset for " + aID); + is(endOffset.value, Math.max(expectedStart, expectedEnd), + msgStart + ": Wrong end offset for " + aID); + } + + /** + * Test adding selections to accessibles. + * @param {string} aID The ID of the element to test. + * @param {array} aSelections Array of selection start and end indices. + */ + async function addSelections(aID, aSelections) { + info("Test adding selections to " + aID); + const hyperText = getAccessible(aID, [ nsIAccessibleText ]); + const initialSelectionCount = hyperText.selectionCount; + + // Multiple selection changes will be coalesced, so just listen for one. + const selectionChange = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID); + for (let [startOffset, endOffset] of aSelections) { + hyperText.addSelection(startOffset, endOffset); + } + await selectionChange; + + is(hyperText.selectionCount, + aSelections.length + initialSelectionCount, + "addSelection: Wrong selection count for " + aID); + + for (let i in aSelections) { + testSelectionBounds(aID, hyperText, initialSelectionCount + i, + aSelections[i], "addSelection"); + } + + is(hyperText.caretOffset, aSelections[hyperText.selectionCount -1][1], + "addSelection: caretOffset not at selection end for " + aID); + } + + /** + * Test changing selections in accessibles. + * @param {string} aID The ID of the element to test. + * @param {int} aIndex The index of the selection to change. + * @param {array} aSelection Array of the selection's new start and end + * indices. + */ + async function changeSelection(aID, aIndex, aSelection) { + info("Test changing the selection of " + aID + " at index " + aIndex); + const [startOffset, endOffset] = aSelection; + const hyperText = getAccessible(aID, [ nsIAccessibleText ]); + + const selectionChanged = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID); + hyperText.setSelectionBounds(aIndex, startOffset, endOffset); + await selectionChanged; + + testSelectionBounds(aID, hyperText, aIndex, + aSelection, "setSelectionBounds"); + + is(hyperText.caretOffset, endOffset, + "setSelectionBounds: caretOffset not at selection end for " + aID); + } + + /** + * Test removing all selections from accessibles. + * @param {string} aID The ID of the element to test. + */ + async function removeSelections(aID) { + info("Testing removal of all selections from " + aID); + const hyperText = getAccessible(aID, [ nsIAccessibleText ]); + + let selectionsRemoved = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, document); + const selectionCount = hyperText.selectionCount; + for (let i = 0; i < selectionCount; i++) { + hyperText.removeSelection(0); + } + await selectionsRemoved; + + is(hyperText.selectionCount, 0, + "removeSelection: Wrong selection count for " + aID); + } + + /** + * Test that changing the DOM selection is reflected in the accessibles. + * @param {string} aID The container ID to test in + * @param {string} aNodeID1 The start node of the selection + * @param {int} aNodeOffset1 The offset where the selection should start + * @param {string} aNodeID2 The node in which the selection should end + * @param {int} aNodeOffset2 The index at which the selection should end + * @param {array} aTests An array of accessibles and their start and end + * offsets to test. + */ + async function changeDOMSelection(aID, aNodeID1, aNodeOffset1, + aNodeID2, aNodeOffset2, + aTests) { + info("Test that DOM selection changes are reflected in the accessibles"); + + let selectionChanged = waitForEvent(EVENT_TEXT_SELECTION_CHANGED, aID); + // HyperTextAccessible::GetSelectionDOMRanges ignores hidden selections. + // Here we may be focusing an editable element (and thus hiding the + // main document selection), so blur it so that we test what we want to + // test. + document.activeElement.blur(); + + const sel = window.getSelection(); + const range = document.createRange(); + range.setStart(getNode(aNodeID1), aNodeOffset1); + range.setEnd(getNode(aNodeID2), aNodeOffset2); + sel.addRange(range); + await selectionChanged; + + for (let i = 0; i < aTests.length; i++) { + const text = getAccessible(aTests[i][0], nsIAccessibleText); + is(text.selectionCount, 1, + "setSelectionBounds: Wrong selection count for " + aID); + testSelectionBounds(aID, text, 0, [aTests[i][1], aTests[i][2]], + "setSelectionBounds"); + } + } + + /** + * Test expected and unexpected events for selecting + * all text and focusing both an input and text area. We expect a caret + * move, but not a text selection change. + * @param {string} aID The ID of the element to test. + */ + async function eventsForSelectingAllTextAndFocus(aID) { + info("Test expected caretMove and unexpected textSelection events for " +aID); + let events = waitForEvents({ + expected: [[EVENT_TEXT_CARET_MOVED, aID]], + unexpected: [[EVENT_TEXT_SELECTION_CHANGED, aID]]}, aID); + selectAllTextAndFocus(aID); + await events; + } + + /** + * Do tests + */ + + async function doTests() { + await addSelections("paragraph", [[1, 3], [6, 10]]); + await changeSelection("paragraph", 0, [2, 4]); + await removeSelections("paragraph"); + + // reverse selection + await addSelections("paragraph", [[1, 3], [10, 6]]); + await removeSelections("paragraph"); + + await eventsForSelectingAllTextAndFocus("textbox"); + await changeSelection("textbox", 0, [1, 3]); + + // reverse selection + await changeSelection("textbox", 0, [3, 1]); + + await eventsForSelectingAllTextAndFocus("textarea"); + await changeSelection("textarea", 0, [1, 3]); + + await changeDOMSelection("c1", "c1_span1", 0, "c1_span2", 0, + [["c1", 2, 2]]); + await changeDOMSelection("c2", "c2", 0, "c2_div2", 1, + [["c2", 0, 3], ["c2_div2", 0, 2]]); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=688126" + title="nsIAccessibleText::setSelectionBounds doesn't fire text selection changed events in some cases"> + Bug 688126 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=688124" + title="no text selection changed event when selection is removed"> + Bug 688124 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <p id="paragraph">hello world</p> + <input id="textbox" value="hello"/> + <textarea id="textarea">hello</textarea> + <div id="c1">hi<span id="c1_span1"></span><span id="c1_span2"></span>hi</div> + <div id="c2">hi<div id="c2_div2">hi</div></div> + +</body> +</html> diff --git a/accessible/tests/mochitest/textselection/test_userinput.html b/accessible/tests/mochitest/textselection/test_userinput.html new file mode 100644 index 0000000000..1aa0b2abb6 --- /dev/null +++ b/accessible/tests/mochitest/textselection/test_userinput.html @@ -0,0 +1,76 @@ +<html> + +<head> + <title>Text selection by user input</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function doTests() { + // Tab to 't2' and then tab out of it: it must not have any selection. + info("Select all text in t1 and focus it"); + let focused = waitForEvent(EVENT_FOCUS, "t1"); + // Simulate tabbing to t1 by selecting all text before focusing it. + selectAllTextAndFocus("t1"); + await focused; + + info("Tab to t2"); + const t2 = getNode("t2"); + focused = waitForEvent(EVENT_FOCUS, t2); + synthesizeKey("VK_TAB"); + await focused; + + info("Tab to t3 and make sure there is no selection in t2 afterwards"); + const t3 = getNode("t3"); + focused = waitForEvent(EVENT_FOCUS, t3); + synthesizeKey("VK_TAB"); + await focused; + const prevFocus = getAccessible(t2, [ nsIAccessibleText ]); + is(prevFocus.selectionCount, 0, + "Wrong selection count for t2"); + + let exceptionCaught = false; + try { + const startOffsetObj = {}, endOffsetObj = {}; + prevFocus.getSelectionBounds(0, startOffsetObj, endOffsetObj); + } catch (e) { + exceptionCaught = true; + } + + ok(exceptionCaught, "No selection was expected for t2"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=440590" + title="Text selection information is not updated when HTML and XUL entries lose focus"> + Bug 440590 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input type="text" id="t1" maxlength="3" size="3" value="1"> + <input type="text" id="t2" maxlength="3" size="3" value="1"> + <input type="text" id="t3" maxlength="3" size="3" value="1"> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/a11y.ini b/accessible/tests/mochitest/tree/a11y.ini new file mode 100644 index 0000000000..a229eeca83 --- /dev/null +++ b/accessible/tests/mochitest/tree/a11y.ini @@ -0,0 +1,56 @@ +[DEFAULT] +support-files = + dockids.html + wnd.xhtml + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/formimage.png + !/accessible/tests/mochitest/letters.gif + !/accessible/tests/mochitest/moz.png + !/accessible/tests/mochitest/tree/wnd.xhtml + !/dom/media/test/bug461281.ogg + +[test_applicationacc.xhtml] +skip-if = true # Bug 561508 +[test_aria_globals.html] +[test_aria_grid.html] +[test_aria_imgmap.html] +[test_aria_list.html] +[test_aria_menu.html] +[test_aria_owns.html] +[test_aria_presentation.html] +[test_aria_table.html] +[test_brokencontext.html] +[test_button.xhtml] +[test_canvas.html] +[test_combobox.xhtml] +[test_cssflexbox.html] +[test_cssoverflow.html] +[test_display_contents.html] +[test_divs.html] +[test_dochierarchy.html] +[test_dockids.html] +[test_filectrl.html] +[test_formctrl.html] +[test_formctrl.xhtml] +[test_gencontent.html] +[test_groupbox.xhtml] +[test_iframe.html] +[test_image.xhtml] +[test_img.html] +[test_invalid_img.xhtml] +[test_invalidationlist.html] +[test_list.html] +[test_map.html] +[test_media.html] +[test_select.html] +[test_svg.html] +[test_tabbox.xhtml] +[test_tabbrowser.xhtml] +skip-if = (os == 'linux' && debug) || (os == 'win' && ccov) # Bug 1389365 || bug 1423218 +[test_table.html] +[test_table_2.html] +[test_table_3.html] +[test_tree.xhtml] +[test_txtcntr.html] +[test_txtctrl.html] +[test_txtctrl.xhtml] diff --git a/accessible/tests/mochitest/tree/dockids.html b/accessible/tests/mochitest/tree/dockids.html new file mode 100644 index 0000000000..1572584689 --- /dev/null +++ b/accessible/tests/mochitest/tree/dockids.html @@ -0,0 +1,30 @@ +<!doctype html> +<html> + <head> + <link rel="next" href="http://www.mozilla.org"> + <style> + head, link, a { display: block; } + link:after { content: "Link to " attr(href); } + </style> + <script> + window.onload = function() { + document.documentElement.appendChild(document.createElement("input")); + + var l = document.createElement("link"); + l.href = "http://www.mozilla.org"; + l.textContent = "Another "; + document.documentElement.appendChild(l); + + l = document.createElement("a"); + l.href = "http://www.mozilla.org"; + l.textContent = "Yet another link to mozilla"; + document.documentElement.appendChild(l); + }; + </script> + </head> + <body> + Hey, I'm a <body> with three links that are not inside me and an input + that's not inside me. + </body> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_applicationacc.xhtml b/accessible/tests/mochitest/tree/test_applicationacc.xhtml new file mode 100644 index 0000000000..5e00a2878f --- /dev/null +++ b/accessible/tests/mochitest/tree/test_applicationacc.xhtml @@ -0,0 +1,73 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible Application Accessible hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + // Note: bug 560239 can be tested if this test runs in standalone mode only. + + var gURL = "../tree/wnd.xhtml" + var gWnd = window.openDialog(gURL, "wnd", "chrome,width=600,height=600"); + + function doTest() + { + // Application accessible should contain two root document accessibles, + // one is for browser window, another one is for open dialog window. + var accTree = { + role: ROLE_APP_ROOT, + children: [ + { + role: ROLE_CHROME_WINDOW, + name: "Accessibility Chrome Test Harness - Minefield" + }, + { + role: ROLE_CHROME_WINDOW, + name: "Empty Window" + } + ] + }; + testAccessibleTree(getApplicationAccessible(), accTree); + + gWnd.close(); + + SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + + // We need to open dialog window before accessibility is started. + addLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=560239" + title="no children of application accessible for windows open before accessibility was started"> + Mozilla Bug 560239 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/tree/test_aria_globals.html b/accessible/tests/mochitest/tree/test_aria_globals.html new file mode 100644 index 0000000000..f7180b7bb1 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_aria_globals.html @@ -0,0 +1,124 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test Global ARIA States and Accessible Creation</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + var globalIds = [ + "atomic", + "busy", + "controls", + "describedby", + "disabled", + "dropeffect", + "flowto", + "grabbed", + "haspopup", + "invalid", + "label", + "labelledby", + "live", + "owns", + "relevant", + ]; + + // Elements having ARIA global state or properties or referred by another + // element must be accessible. + ok(isAccessible("pawn"), + "Must be accessible because referred by another element."); + + for (let idx = 0; idx < globalIds.length; idx++) { + ok(isAccessible(globalIds[idx]), + "Must be accessible becuase of aria-" + globalIds[idx] + + " presence"); + } + + // Unfocusable elements, having ARIA global state or property with a valid + // IDREF value, and an inherited presentation role. A generic accessible + // is created (to prevent table cells text jamming). + ok(!isAccessible("td_nothing", nsIAccessibleTableCell), + "inherited presentation role takes a place"); + + for (let idx = 0; idx < globalIds.length; idx++) { + ok(isAccessible("td_" + globalIds[idx]), + "Inherited presentation role must be ignored becuase of " + + "aria-" + globalIds[idx] + " presence"); + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Update universal ARIA attribute support to latest spec" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=551978"> + Mozilla Bug 551978 + </a> + <a target="_blank" + title="Presentational table related elements referred or having global ARIA attributes must be accessible" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=809751"> + Mozilla Bug 809751 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Test that global aria states and properties are enough to cause the + creation of accessible objects --> + <div id="global_aria_states_and_props" role="group"> + <span id="pawn"></span> + <span id="atomic" aria-atomic="true"></span> + <span id="busy" aria-busy="false"></span> + <span id="controls" aria-controls="pawn"></span> + <span id="describedby" aria-describedby="pawn"></span> + <span id="disabled" aria-disabled="true"></span> + <span id="dropeffect" aria-dropeffect="move"></span> + <span id="flowto" aria-flowto="pawn"></span> + <span id="grabbed" aria-grabbed="false"></span> + <span id="haspopup" aria-haspopup="false"></span> + <span id="invalid" aria-invalid="false"></span> + <span id="label" aria-label="hi"></span> + <span id="labelledby" aria-labelledby="label"></span> + <span id="live" aria-live="polite"></span> + <span id="owns" aria-owns="pawn"></span> + <span id="relevant" aria-relevant="additions"></span> + </div> + + <table role="presentation"> + <tr> + <td id="td_nothing"></td> + <td id="td_atomic" aria-atomic="true"></td> + <td id="td_busy" aria-busy="false"></td> + <td id="td_controls" aria-controls="pawn"></td> + <td id="td_describedby" aria-describedby="pawn"></td> + <td id="td_disabled" aria-disabled="true"></td> + <td id="td_dropeffect" aria-dropeffect="move"></td> + <td id="td_flowto" aria-flowto="pawn"></td> + <td id="td_grabbed" aria-grabbed="false"></td> + <td id="td_haspopup" aria-haspopup="false"></td> + <td id="td_invalid" aria-invalid="false"></td> + <td id="td_label" aria-label="hi"></td> + <td id="td_labelledby" aria-labelledby="label"></td> + <td id="td_live" aria-live="polite"></td> + <td id="td_owns" aria-owns="pawn"></td> + <td id="td_relevant" aria-relevant="additions"></td> + </tr> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_aria_grid.html b/accessible/tests/mochitest/tree/test_aria_grid.html new file mode 100644 index 0000000000..80ff97095b --- /dev/null +++ b/accessible/tests/mochitest/tree/test_aria_grid.html @@ -0,0 +1,318 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML table tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // grid having rowgroups + + var accTree = + { TABLE: [ + { GROUPING: [ + { ROW: [ + { GRID_CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }, + ] }; + + testAccessibleTree("grid", accTree); + + // //////////////////////////////////////////////////////////////////////// + // strange grids (mix of ARIA and HTML tables) + + accTree = { + role: ROLE_TABLE, + children: [ + { // div@role="row" + role: ROLE_ROW, + tagName: "DIV", + children: [ + { // caption text leaf + role: ROLE_TEXT_LEAF, + name: "caption", + children: [ ], + }, + { // th generic accessible + role: ROLE_TEXT_CONTAINER, + children: [ + { // th text leaf + role: ROLE_TEXT_LEAF, + name: "header1", + children: [ ], + }, + ], + }, + { // td@role="columnheader" + role: ROLE_COLUMNHEADER, + name: "header2", + children: [ { TEXT_LEAF: [ ] } ], + }, + ], + }, + ], + }; + testAccessibleTree("strange_grid1", accTree); + + accTree = { + role: ROLE_TABLE, + children: [ + { // tr@role="row" + role: ROLE_ROW, + tagName: "TR", + children: [ + { // td implicit role="gridcell" + role: ROLE_GRID_CELL, + children: [ + { // td text leaf + role: ROLE_TEXT_LEAF, + name: "cell1", + children: [ ], + }, + ], + }, + { // td@role="gridcell" + role: ROLE_GRID_CELL, + name: "cell2", + children: [ { TEXT_LEAF: [ ] } ], + }, + ], + }, + ], + }; + testAccessibleTree("strange_grid2", accTree); + + accTree = { + role: ROLE_TABLE, + children: [ + { // div@role="row" + role: ROLE_ROW, + children: [ + { // div@role="gridcell" + role: ROLE_GRID_CELL, + children: [ + { // td generic accessible + role: ROLE_TEXT_CONTAINER, + children: [ + { // text leaf from presentational table + role: ROLE_TEXT_LEAF, + name: "cell3", + children: [ ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + testAccessibleTree("strange_grid3", accTree); + + accTree = { + role: ROLE_TABLE, + children: [ + { // div@role="row" + role: ROLE_ROW, + children: [ + { // div@role="gridcell" + role: ROLE_GRID_CELL, + children: [ + { // table + role: ROLE_TABLE, + children: [ + { // tr + role: ROLE_ROW, + children: [ + { // td + role: ROLE_CELL, + children: [ + { // caption text leaf of presentational table + role: ROLE_TEXT_LEAF, + name: "caption", + children: [ ], + }, + { // td generic accessible + role: ROLE_TEXT_CONTAINER, + children: [ + { // td text leaf of presentational table + role: ROLE_TEXT_LEAF, + name: "cell4", + children: [ ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }, + ], + }; + + testAccessibleTree("strange_grid4", accTree); + + // //////////////////////////////////////////////////////////////////////// + // grids that could contain whitespace accessibles but shouldn't. + + accTree = + { TREE_TABLE: [ + { ROW: [ + { GRID_CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + { GRID_CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + { GRID_CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + + testAccessibleTree("whitespaces-grid", accTree); + + // grids that could contain text container accessibles but shouldn't. + + accTree = + { TABLE: [ + { ROW: [ + { GRID_CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + { GRID_CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + { ROW: [ + { GRID_CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + { GRID_CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + + testAccessibleTree("gridWithPresentationalBlockElement", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Support ARIA role rowgroup" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=525909"> + Mozilla Bug 525909 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="grid" role="grid"> + <div role="rowgroup"> + <div role="row"> + <div role="gridcell">cell</div> + </div> + </div> + </div> + + <div id="strange_grid1" role="grid"> + <div role="row"> + <table role="presentation"> + <caption>caption</caption> + <tr> + <th>header1</th> + <td role="columnheader">header2</td> + </tr> + </table> + </div> + </div> + + <div id="strange_grid2" role="grid"> + <table role="presentation"> + <tr role="row"> + <td id="implicit_gridcell">cell1</td> + <td role="gridcell">cell2</td> + </tr> + </table> + </div> + + <div id="strange_grid3" role="grid"> + <div role="row"> + <div role="gridcell"> + <table role="presentation"> + <tr> + <td>cell3</td> + </tr> + </table> + </div> + </div> + </div> + + <div id="strange_grid4" role="grid"> + <div role="row"> + <div role="gridcell"> + <table> + <tr> + <td> + <table role="presentation"> + <caption>caption</caption> + <tr><td>cell4</td></tr> + </table> + </td> + </tr> + </table> + </div> + </div> + </div> + + <div role="treegrid" id="whitespaces-grid"> + <div role="row" aria-selected="false" tabindex="-1"> + <span role="gridcell">03:30PM-04:30PM</span> + <span role="gridcell" style="font-weight:bold;">test</span> + <span role="gridcell">a user1</span> + </div> + </div> + + <div id="gridWithPresentationalBlockElement" role="grid"> + <span style="display: block;"> + <div role="row"> + <div role="gridcell">Cell 1</div> + <div role="gridcell">Cell 2</div> + </div> + </span> + <span style="display: block;"> + <div role="row"> + <span style="display: block;"> + <div role="gridcell">Cell 3</div> + <div role="gridcell">Cell 4</div> + </span> + </div> + </span> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_aria_imgmap.html b/accessible/tests/mochitest/tree/test_aria_imgmap.html new file mode 100644 index 0000000000..0cd4867cae --- /dev/null +++ b/accessible/tests/mochitest/tree/test_aria_imgmap.html @@ -0,0 +1,104 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test usemap elements and ARIA</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + function doPreTest() { + waitForImageMap("imagemap", doTest); + } + + function doTest() { + var accTree = { + role: ROLE_IMAGE_MAP, + children: [ + { + role: ROLE_ENTRY, + name: "first name", + }, + { + role: ROLE_ENTRY, + name: "last name", + }, + { + role: ROLE_RADIOBUTTON, + name: "male", + }, + { + role: ROLE_RADIOBUTTON, + name: "female", + }, + { + role: ROLE_CHECKBUTTON, + name: "have bike", + }, + { + role: ROLE_EDITCOMBOBOX, + name: "bike model", + }, + { + role: ROLE_CHECKBUTTON, + name: "have car", + }, + { + role: ROLE_CHECKBUTTON, + name: "have airplane", + }, + { + role: ROLE_PUSHBUTTON, + name: "submit", + }, + ], + }; + + // Test image map tree structure, roles, and names. + testAccessibleTree("imagemap", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doPreTest); + </script> + +</head> +<body> + +<a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=548291" + title="Accessible tree of ARIA image maps"> +Mozilla Bug 548291 +</a> +<p id="display"></p> +<div id="content" style="display: none"></div> +<pre id="test"> +</pre> + +<img id="imagemap" src="../formimage.png" width="219" height="229" border="0" usemap="#ariaMap"> +<map id="ariaMap" name="ariaMap"> + <area id="t1" role="textbox" shape="rect" tabindex="0" alt="" title="first name" coords="4,20,108,48" href="#" /> + <area id="t2" role="textbox" shape="rect" alt="" title="last name" coords="111,21,215,50" href="#" /> + <area id="rb1" role="radio" aria-checked="true" shape="circle" alt="" title="male" coords="60,75,11" href="#" /> + <area id="rb2" role="radio" shape="circle" alt="" title="female" coords="73,94,11" href="#" /> + <area id="cb1" role="checkbox" aria-checked="true" shape="rect" alt="" title="have bike" coords="95,123,118,145" href="#" /> + <area id="cbox" role="combobox" shape="rect" alt="" title="bike model" coords="120,124,184,146" href="#" /> + <area id="cb2" role="checkbox" shape="rect" alt="" title="have car" coords="90,145,114,164" href="#" /> + <area id="cb3" role="checkbox" shape="rect" alt="" title="have airplane" coords="130,163,152,184" href="#" /> + <area id="b1" role="button" shape="rect" alt="" title="submit" coords="4,198,67,224" href="#" /> +</map> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_aria_list.html b/accessible/tests/mochitest/tree/test_aria_list.html new file mode 100644 index 0000000000..f3642c801d --- /dev/null +++ b/accessible/tests/mochitest/tree/test_aria_list.html @@ -0,0 +1,90 @@ +<!DOCTYPE html> +<html> +<head> + <title>ARIA lists</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // list + + var accTree = + { LIST: [ + { LISTITEM: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }; + + testAccessibleTree("list", accTree); + + // //////////////////////////////////////////////////////////////////////// + // strange list (mix of ARIA and HTML) + + accTree = { // div@role="list" + role: ROLE_LIST, + children: [ + { // li + role: ROLE_TEXT_CONTAINER, + children: [ + { // li text leaf + role: ROLE_TEXT_LEAF, + name: "item1", + children: [ ], + }, + ], + }, + { // li@role="listitem" + role: ROLE_LISTITEM, + children: [ + { // text leaf + role: ROLE_TEXT_LEAF, + name: "item2", + children: [ ], + }, + ], + }, + ], + }; + + testAccessibleTree("strange_list", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Build the context dependent tree" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=804461"> + Mozilla Bug 804461 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="list" role="list"> + <div role="listitem">item1</div> + </div> + + <div id="strange_list" role="list"> + <ul role="presentation"> + <li>item1</li> + <li role="listitem">item2</li> + </ul> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_aria_menu.html b/accessible/tests/mochitest/tree/test_aria_menu.html new file mode 100644 index 0000000000..2f1f6645db --- /dev/null +++ b/accessible/tests/mochitest/tree/test_aria_menu.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test accessible tree when ARIA role menuitem is used</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // Menuitem with no popup. + let tree = + { SECTION: [ // container + { MENUPOPUP: [ // menu + { MENUITEM: [ + { LISTITEM_MARKER: [] }, // bullet + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }; + testAccessibleTree("menu", tree); + + // Menuitem with explicit no popup. + tree = + { SECTION: [ // container + { MENUPOPUP: [ // menu + { MENUITEM: [ + { LISTITEM_MARKER: [] }, // bullet + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }; + testAccessibleTree("menu_nopopup", tree); + + // Menuitem with popup. + tree = + { SECTION: [ // container + { MENUPOPUP: [ // menu + { PARENT_MENUITEM: [ // menuitem with aria-haspopup="true" + { LISTITEM_MARKER: [] }, // bullet + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }; + testAccessibleTree("menu_popup", tree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=786566" + title="ARIA menuitem acting as submenu should have PARENT_MENUITEM role"> + Mozilla Bug 786566 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="menu"> + <ul role="menu"> + <li role="menuitem">Normal Menu</li> + </ul> + </div> + + <div id="menu_nopopup"> + <ul role="menu"> + <li role="menuitem" aria-haspopup="false">Menu with explicit no popup</li> + </ul> + </div> + + <div id="menu_popup"> + <ul role="menu"> + <li role="menuitem" aria-haspopup="true">Menu with popup</li> + </ul> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_aria_owns.html b/accessible/tests/mochitest/tree/test_aria_owns.html new file mode 100644 index 0000000000..a01968521b --- /dev/null +++ b/accessible/tests/mochitest/tree/test_aria_owns.html @@ -0,0 +1,197 @@ +<!DOCTYPE html> +<html> + +<head> + <title>@aria-owns attribute testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Tests + // ////////////////////////////////////////////////////////////////////////// + + // enableLogging("tree,verbose"); // debug stuff + + var gQueue = null; + + function doTest() { + var tree = + { SECTION: [ // t1_1 + { HEADING: [ // t1_2 + // no kids, no loop + ] }, + ] }; + testAccessibleTree("t1_1", tree); + + tree = + { SECTION: [ // t2_1 + { GROUPING: [ // t2_2 + { HEADING: [ // t2_3 + // no kids, no loop + ] }, + ] }, + ] }; + testAccessibleTree("t2_1", tree); + + tree = + { SECTION: [ // t3_3 + { GROUPING: [ // t3_1 + { NOTE: [ // t3_2 + { HEADING: [ // DOM child of t3_2 + // no kids, no loop + ] }, + ] }, + ] }, + ] }; + testAccessibleTree("t3_3", tree); + + tree = + { SECTION: [ // t4_1 + { GROUPING: [ // DOM child of t4_1, aria-owns ignored + // no kids, no loop + ] }, + ] }; + testAccessibleTree("t4_1", tree); + + tree = + { SECTION: [ // t5_1 + { GROUPING: [ // DOM child of t5_1 + { NOTE: [ // t5_2 + { HEADING: [ // DOM child of t5_2 + { FORM: [ // t5_3 + { TOOLTIP: [ // DOM child of t5_3 + // no kids, no loop + ]}, + ]}, + ]}, + ] }, + ] }, + ] }; + testAccessibleTree("t5_1", tree); + + tree = + { SECTION: [ // t6_1 + { RADIOBUTTON: [ ] }, + { CHECKBUTTON: [ ] }, // t6_3, rearranged by aria-owns + { PUSHBUTTON: [ ] }, // t6_2, rearranged by aria-owns + ] }; + testAccessibleTree("t6_1", tree); + + tree = + { SECTION: [ // ariaowns_container + { SECTION: [ // ariaowns_self + { SECTION: [ // ariaowns_uncle + ] }, + ] }, + ] }; + testAccessibleTree("ariaowns_container", tree); + + tree = + { TABLE: [ + { ROW: [ + { GRID_CELL: [ + { TEXT_LEAF: [] }, + ] }, + { GRID_CELL: [ + { TEXT_LEAF: [] }, + ] }, + ] }, + { ROW: [ + { GRID_CELL: [ + { TEXT_LEAF: [] }, + ] }, + { GRID_CELL: [ + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }; + testAccessibleTree("grid", tree); + + tree = + { SECTION: [ // presentation_owner + // Can't own ancestor, so no children. + ] }; + testAccessibleTree("presentation_owner", tree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + + </script> +</head> + +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- simple loop --> + <div id="t1_1" aria-owns="t1_2"></div> + <div id="t1_2" aria-owns="t1_1" role="heading"></div> + + <!-- loop --> + <div id="t2_2" aria-owns="t2_3" role="group"></div> + <div id="t2_1" aria-owns="t2_2"></div> + <div id="t2_3" aria-owns="t2_1" role="heading"></div> + + <!-- loop #2 --> + <div id="t3_1" aria-owns="t3_2" role="group"></div> + <div id="t3_2" role="note"> + <div aria-owns="t3_3" role="heading"></div> + </div> + <div id="t3_3" aria-owns="t3_1"></div> + + <!-- self loop --> + <div id="t4_1"><div aria-owns="t4_1" role="group"></div></div> + + <!-- natural and aria-owns hierarchy --> + <div id="t5_2" role="note"><div aria-owns="t5_3" role="heading"></div></div> + <div id="t5_1"><div aria-owns="t5_2" role="group"></div></div> + <div id="t5_3" role="form"><div aria-owns="t5_1" role="tooltip"></div></div> + + <!-- rearrange children --> + <div id="t6_1" aria-owns="t6_3 t6_2"> + <div id="t6_2" role="button"></div> + <div id="t6_3" role="checkbox"></div> + <div role="radio"></div> + </div> + + <div id="ariaowns_container"> + <div id="ariaowns_self" + aria-owns="aria_ownscontainer ariaowns_self ariaowns_uncle"></div> + </div> + <div id="ariaowns_uncle"></div> + + <!-- grid --> + <div aria-owns="grid-row2" role="grid" id="grid"> + <div role="row"> + <div role="gridcell">cell 1,1</div> + <div role="gridcell">cell 1,2</div> + </div> + </div> + <div role="row" id="grid-row2"> + <div role="gridcell">cell 2,1</div> + <div role="gridcell">cell 2,2</div> + </div> + + <!-- Owned child which is an ancestor of its owner but didn't yet exist when + aria-owns relocation was processed (bug 1485097). --> + <div id="presentation" role="presentation"> + <div id="presentation_owner" aria-owns="presentation"></div> + </div> +</body> + +</html> diff --git a/accessible/tests/mochitest/tree/test_aria_presentation.html b/accessible/tests/mochitest/tree/test_aria_presentation.html new file mode 100644 index 0000000000..5680193441 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_aria_presentation.html @@ -0,0 +1,176 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test accessible tree when ARIA role presentation is used</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // Presentation role don't allow accessible. + var tree = + { SECTION: [ // container + { TEXT_LEAF: [ ] }, // child text of 'presentation' node + { TEXT_LEAF: [ ] }, // child text of 'none' node + ] }; + testAccessibleTree("div_cnt", tree); + + // Focusable element, 'presentation' and 'none' roles are ignored. + tree = + { SECTION: [ // container + { PUSHBUTTON: [ // button having 'presentation' role + { TEXT_LEAF: [ ] }, + ] }, + { PUSHBUTTON: [ // button having 'none' role + { TEXT_LEAF: [ ] }, + ] }, + ] }; + testAccessibleTree("btn_cnt", tree); + + // Presentation table, no table structure is exposed. + tree = + { SECTION: [ // container + { TEXT_CONTAINER: [ // td generic accessible inside 'presentation' table + { TEXT_LEAF: [ ] }, // cell text + ] }, + { TEXT_CONTAINER: [ // td generic accessible inside 'none' table + { TEXT_LEAF: [ ] }, // cell text + ] }, + ] }; + testAccessibleTree("tbl_cnt", tree); + + // Focusable table, 'presentation' and 'none' roles are ignored. + tree = + { SECTION: [ // container + { TABLE: [ // table having 'presentation' role + { ROW: [ // tr + { CELL: [ // td + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }, + { TABLE: [ // table having 'none' role + { ROW: [ // tr + { CELL: [ // td + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }, + ] }; + testAccessibleTree("tblfocusable_cnt", tree); + + // Presentation list, expose generic accesisble for list items. + tree = + { SECTION: [ // container + { TEXT_CONTAINER: [ // li generic accessible inside 'presentation' role + { TEXT_LEAF: [ ] }, // li text + ] }, + { TEXT_CONTAINER: [ // li generic accessible inside 'none' role + { TEXT_LEAF: [ ] }, // li text + ] }, + ] }; + testAccessibleTree("list_cnt", tree); + + // Has ARIA globals or referred by ARIA relationship, role='presentation' + // and role='none' are ignored. + tree = + { SECTION: [ // container + { LABEL: [ // label, has aria-owns + { TEXT_LEAF: [ ] }, + { LABEL: [ // label, referenced by aria-owns + { TEXT_LEAF: [ ] }, + ] }, + ] }, + { LABEL: [ // label, has aria-owns + { TEXT_LEAF: [ ] }, + { LABEL: [ // label, referenced by aria-owns + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree("airaglobalprop_cnt", tree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=548291" + title="Accessible tree of ARIA image maps"> + Bug 548291 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=666504" + title="Ignore role presentation on focusable elements"> + Bug 666504 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=971212" + title="Implement ARIA role=none"> + Bug 971212 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="div_cnt"><div role="presentation">t</div><div role="none">t</div></div> + + <div id="btn_cnt"><button role="presentation">btn</button><button role="none">btn</button></div> + + <div id="tbl_cnt"> + <table role="presentation"> + <tr> + <td>cell</td> + </tr> + </table> + <table role="none"> + <tr> + <td>cell</td> + </tr> + </table> + </div> + + <div id="tblfocusable_cnt"> + <table role="presentation" tabindex="0"> + <tr> + <td>cell</td> + </tr> + </table> + <table role="none" tabindex="0"> + <tr> + <td>cell</td> + </tr> + </table> + </div> + + <div id="list_cnt"> + <ul role="presentation"> + <li>item</li> + </ul> + <ul role="none"> + <li>item</li> + </ul> + </div> + + <div id="airaglobalprop_cnt"><label + role="presentation" aria-owns="ariaowned">has aria-owns</label><label + role="presentation" id="ariaowned">referred by aria-owns</label><label + role="none" aria-owns="ariaowned2">has aria-owns</label><label + role="none" id="ariaowned2">referred by aria-owns</label></div> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_aria_table.html b/accessible/tests/mochitest/tree/test_aria_table.html new file mode 100644 index 0000000000..22375faf59 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_aria_table.html @@ -0,0 +1,101 @@ +<!DOCTYPE html> +<html> +<head> + <title>ARIA table tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // table having rowgroups + + var accTree = + { TABLE: [ + { GROUPING: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }, + ] }; + + testAccessibleTree("table", accTree); + + // tables that could contain text container accessibles but shouldn't. + + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + + testAccessibleTree("tableWithPresentationalBlockElement", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="support ARIA table and cell roles" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1173364"> + Bug 1173364 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="table" role="table"> + <div role="rowgroup"> + <div role="row"> + <div role="cell">cell</div> + </div> + </div> + </div> + + <div id="tableWithPresentationalBlockElement" 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> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_brokencontext.html b/accessible/tests/mochitest/tree/test_brokencontext.html new file mode 100644 index 0000000000..fbdefd398f --- /dev/null +++ b/accessible/tests/mochitest/tree/test_brokencontext.html @@ -0,0 +1,214 @@ +<!DOCTYPE html> +<html> +<head> + <title>Broken context hierarchy</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + /** + * Return true if TD element has a generic accessible. + */ + function isTDGeneric(aID) { + return isAccessible(aID) && !isAccessible(aID, nsIAccessibleTableCell); + } + + function checkIfNotAccessible(aID) { + ok(!isAccessible(aID), "'" + aID + "' shouldn't be accessible"); + } + function checkIfTDGeneric(aID) { + ok(isTDGeneric(aID), "'" + aID + "' shouldn't have cell accessible"); + } + + function doTest() { + // ////////////////////////////////////////////////////////////////////////// + // HTML table elements outside table context. + + // HTML table role="presentation" + checkIfNotAccessible("tr_in_presentation_table"); + checkIfTDGeneric("th_in_presentation_table"); + checkIfTDGeneric("td_in_presentation_table"); + + // HTML table role="button" + var tree = + { PUSHBUTTON: [ // table + { TEXT_CONTAINER: [ // tr + { TEXT_CONTAINER: [ // th + { TEXT_LEAF: [ ] }, + ] }, + { TEXT_CONTAINER: [ // td + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree("button_table", tree); + + // ////////////////////////////////////////////////////////////////////////// + // HTML list elements outside list context. + + ok(!isAccessible("presentation_ul"), + "presentational ul shouldn't be accessible"); + ok(isAccessible("item_in_presentation_ul"), + "li in presentational ul should have generic accessible"); + ok(isAccessible("styleditem_in_presentation_ul"), + "list styled span in presentational ul should have generic accessible"); + + ok(!isAccessible("presentation_ol"), + "presentational ol shouldn't be accessible"); + ok(isAccessible("item_in_presentation_ol"), + "li in presentational ol should have generic accessible"); + + ok(!isAccessible("presentation_dl"), + "presentational dl shouldn't be accessible"); + ok(!isAccessible("dt_in_presentation_dl"), + "dt in presentational dl shouldn't be accessible"); + ok(!isAccessible("dd_in_presentation_dl"), + "dd in presentational dl shouldn't be accessible"); + + tree = + { PUSHBUTTON: [ // ul + { TEXT_CONTAINER: [ // li + { LISTITEM_MARKER: [ ] }, + { TEXT_LEAF: [ ] }, + ] }, + { TEXT_CONTAINER: [ // span styled as a list + { LISTITEM_MARKER: [ ] }, + { TEXT_LEAF: [ ] }, + ] }, + ] }; + testAccessibleTree("button_ul", tree); + + tree = + { PUSHBUTTON: [ // ol + { TEXT_CONTAINER: [ // li + { LISTITEM_MARKER: [ ] }, + { TEXT_LEAF: [ ] }, + ] }, + ] }; + testAccessibleTree("button_ol", tree); + + tree = + { PUSHBUTTON: [ // dl + { TEXT_CONTAINER: [ // dt + { TEXT_LEAF: [ ] }, + ] }, + { TEXT_CONTAINER: [ // dd + { TEXT_LEAF: [ ] }, + ] }, + ] }; + testAccessibleTree("button_dl", tree); + + // ////////////////////////////////////////////////////////////////////////// + // Styled as HTML table elements, accessible is created by tag name + + tree = + { LINK: [ // a + { TEXT_LEAF: [ ] }, + ] }; + testAccessibleTree("a_as_td", tree); + + tree = + { HEADING: [ + { TEXT_LEAF: [ ] }, + ] }; + testAccessibleTree("h1_as_td", tree); + testAccessibleTree("h2_as_td", tree); + testAccessibleTree("h3_as_td", tree); + testAccessibleTree("h4_as_td", tree); + testAccessibleTree("h5_as_td", tree); + testAccessibleTree("h6_as_td", tree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=706849" + title="Create accessible by tag name as fallback if table descendant style is used out of table context"> + Bug 706849 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=804461" + title="Build the context dependent tree "> + Bug 804461 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=945435" + title="Create generic accessible for td to not jamm the cell text"> + Bug 945435 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- HTML table elements out of table --> + <table role="presentation"> + <tr id="tr_in_presentation_table"> + <th id="th_in_presentation_table">not a header</th> + <td id="td_in_presentation_table">not a cell</td> + </tr> + </table> + + <table role="button" id="button_table"> + <tr id="tr_in_button_table"> + <th id="th_in_button_table">not a header</th> + <td id="td_in_button_table">not a cell</td> + </tr> + </table> + + <!-- HTML list elements out of list --> + <ul role="presentation" id="presentation_ul"> + <li id="item_in_presentation_ul">item</li> + <span id="styleditem_in_presentation_ul" + style="display:list-item">Oranges</span> + </ul> + + <ol role="presentation" id="presentation_ol"> + <li id="item_in_presentation_ol">item</li> + </ol> + + <dl role="presentation" id="presentation_dl"> + <dt id="dt_in_presentation_dl">term</dt> + <dd id="dd_in_presentation_dl">definition</dd> + </dl> + + <ul role="button" id="button_ul"> + <li id="item_in_button_ul">item</li> + <span id="styleditem_in_button_ul" + style="display:list-item">Oranges</span> + </ul> + + <ol role="button" id="button_ol"> + <li id="item_in_button_ul">item</li> + </ol> + + <dl role="button" id="button_dl"> + <dt id="dt_in_button_dl">term</ld> + <dd id="dd_in_button_dl">definition</dd> + </dl> + + <!-- styled as HTML table elements --> + <a id="a_as_td" style="display:table-cell;" href="http://www.google.com">Google</a> + <h1 id="h1_as_td" style="display: table-cell;">h1</h1> + <h2 id="h2_as_td" style="display: table-cell;">h2</h2> + <h3 id="h3_as_td" style="display: table-cell;">h3</h3> + <h4 id="h4_as_td" style="display: table-cell;">h4</h4> + <h5 id="h5_as_td" style="display: table-cell;">h5</h5> + <h6 id="h6_as_td" style="display: table-cell;">h6</h6> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_button.xhtml b/accessible/tests/mochitest/tree/test_button.xhtml new file mode 100644 index 0000000000..67c81239f5 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_button.xhtml @@ -0,0 +1,72 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL button hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // button + + var accTree = { + role: ROLE_PUSHBUTTON, + name: "hello", + children: [ ] + }; + testAccessibleTree("button1", accTree); + + ////////////////////////////////////////////////////////////////////////// + // toolbarbutton + + accTree = { + role: ROLE_PUSHBUTTON, + name: "hello", + children: [ ] + }; + testAccessibleTree("button2", accTree); + + SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=249292" + title="Ensure accessible children for toolbarbutton types 'menu'"> + Mozilla Bug 249292 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <button id="button1" label="hello"/> + <toolbarbutton id="button2" label="hello"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/tree/test_canvas.html b/accessible/tests/mochitest/tree/test_canvas.html new file mode 100644 index 0000000000..804bcc1f6e --- /dev/null +++ b/accessible/tests/mochitest/tree/test_canvas.html @@ -0,0 +1,53 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=495912 +--> +<head> + <title>File Input Control tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + var accTree = + { CANVAS: [ + { CHECKBUTTON: [] }, + { ENTRY: [] }, + ] }; + + testAccessibleTree("canvas", accTree); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Expose alternative content in Canvas element to ATs" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=495912">Mozilla Bug 495912</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <canvas id="canvas" tabindex="0"><input type="checkbox"><input></canvas> + + <script type="text/javascript"> + var c = document.getElementById("canvas"); + var cxt = c.getContext("2d"); + cxt.fillStyle = "#005500"; + cxt.fillRect(0, 0, 150, 75); + </script> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_combobox.xhtml b/accessible/tests/mochitest/tree/test_combobox.xhtml new file mode 100644 index 0000000000..b08a0e3d83 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_combobox.xhtml @@ -0,0 +1,117 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + xmlns:html="http://www.w3.org/1999/xhtml" + title="Accessible XUL menulist and textbox @autocomplete hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // menulist + + var selectedOptionChildren = []; + if (MAC) { + // checkmark is part of the Mac menu styling + selectedOptionChildren = [{ + role: ROLE_STATICTEXT, + children: [] + }]; + } + + var accTree = { + role: ROLE_COMBOBOX, + children: [ + { + role: ROLE_COMBOBOX_LIST, + children: [ + { + role: ROLE_COMBOBOX_OPTION, + children: selectedOptionChildren + }, + { + role: ROLE_COMBOBOX_OPTION, + children: [] + } + ] + } + ] + }; + + testAccessibleTree("menulist", accTree); + + ////////////////////////////////////////////////////////////////////////// + // textbox@type=autocomplete #1 (history) + + accTree = { + // html:input + role: ROLE_ENTRY, + children: [ + { + // #text + role: ROLE_TEXT_LEAF, + name: "http://mochi.test:8888/redirect-a11y.html", + children: [] + } + ], + }; + + testAccessibleTree("autocomplete", accTree); + + SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=249292" + title="Ensure accessible children for toolbarbutton types 'menu'"> + Mozilla Bug 249292 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=626660" + title="Cache rendered text on a11y side"> + Mozilla Bug 626660 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menulist id="menulist"> + <menupopup> + <menuitem label="item"/> + <menuitem label="item"/> + </menupopup> + </menulist> + + <html:input is="autocomplete-input" + id="autocomplete" + autocompletesearch="unifiedcomplete" + value="http://mochi.test:8888/redirect-a11y.html"/> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/tree/test_cssflexbox.html b/accessible/tests/mochitest/tree/test_cssflexbox.html new file mode 100644 index 0000000000..72fafba0a2 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_cssflexbox.html @@ -0,0 +1,78 @@ +<!DOCTYPE html> +<html> + +<head> + <title>CSS flexbox tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // Ensure that flexbox ordering and absolute positioning do not affect + // the accessibility tree. + // Note that there is no accessible for a div with display:flex style. + var accTree = { + role: ROLE_SECTION, + children: [ + { // Bug 1277559. Button outside the flexed content + role: ROLE_PUSHBUTTON, + name: "Button", + }, + { // Visually first button in the 3 button row + role: ROLE_PUSHBUTTON, + name: "First", + }, + { // Flushed right third button in the 3 button row + role: ROLE_PUSHBUTTON, + name: "Second", + }, + { // Middle button in the 3 button row + role: ROLE_PUSHBUTTON, + name: "Third", + }, // end bug 1277559 + { // Bug 962558: DOM first, Order 2. + role: ROLE_PUSHBUTTON, + name: "two, tab first", + }, + { // DOM order second, flex order 1 + role: ROLE_PUSHBUTTON, + name: "one, tab second", + }, // end bug 962558 + ], + }; + testAccessibleTree("flex_elements", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="flex_elements"> + <button type="button">Button</button> + <div style="position: relative; display: flex; width: 200px;"> + <button type="button" style="order: 1">First</button> + <button type="button" style="order: 2; position: absolute; right: 0">Second</button> + <button type="button" style="order: 3">Third</button> + </div> + <div style="display: flex"> + <button id="two" style="order: 2">two, tab first</button> + <button id="one" style="order: 1">one, tab second</button> + </div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_cssoverflow.html b/accessible/tests/mochitest/tree/test_cssoverflow.html new file mode 100644 index 0000000000..87898fef6c --- /dev/null +++ b/accessible/tests/mochitest/tree/test_cssoverflow.html @@ -0,0 +1,135 @@ +<html> + +<head> + <title>CSS overflow testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style> + a.link:focus { + overflow: scroll; + } + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function focusAnchor(aID) { + this.linkNode = getNode(aID); + this.link = getAccessible(this.linkNode); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getAccessible, this.linkNode), + ]; + + this.invoke = function focusAnchor_invoke() { + this.linkNode.focus(); + }; + + this.check = function focusAnchor_check(aEvent) { + is(this.link, aEvent.accessible, + "Focus should be fired against new link accessible!"); + }; + + this.getID = function focusAnchor_getID() { + return "focus a:focus{overflow:scroll} #1"; + }; + } + + function tabAnchor(aID) { + this.linkNode = getNode(aID); + this.link = getAccessible(this.linkNode); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getAccessible, this.linkNode), + ]; + + this.invoke = function tabAnchor_invoke() { + synthesizeKey("VK_TAB", { shiftKey: false }); + }; + + this.check = function tabAnchor_check(aEvent) { + is(this.link, aEvent.accessible, + "Focus should be fired against new link accessible!"); + }; + + this.getID = function tabAnchor_getID() { + return "focus a:focus{overflow:scroll} #2"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + + var gQueue = null; + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function doTests() { + // Shift+Tab not working, and a test timeout, bug 746977 + if (MAC) { + todo(false, "Shift+tab isn't working on OS X, needs to be disabled until bug 746977 is fixed!"); + SimpleTest.finish(); + return; + } + + gQueue = new eventQueue(); + + // CSS 'overflow: scroll' property setting and unsetting causes accessible + // recreation (and fire show/hide events). For example, the focus and + // blur of HTML:a with ':focus {overflow: scroll; }' CSS style causes its + // accessible recreation. The focus event should be fired on new + // accessible. + gQueue.push(new focusAnchor("a")); + gQueue.push(new tabAnchor("a2")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=591163" + title="mochitest for bug 413777: focus the a:focus {overflow: scroll;} shouldn't recreate HTML a accessible"> + Mozilla Bug 591163 + </a><br> + <a target="_blank" + title="Rework accessible tree update code" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"> + Mozilla Bug 570275 + </a><br> + <a target="_blank" + title="Text control frames should accept dynamic changes to the CSS overflow property" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=686247"> + Mozilla Bug 686247 + </a><br> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <div> + <a id="a" class="link" href="www">link</a> + </div> + <div> + <a id="a2" class="link" href="www">link2</a> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_display_contents.html b/accessible/tests/mochitest/tree/test_display_contents.html new file mode 100644 index 0000000000..371146e5eb --- /dev/null +++ b/accessible/tests/mochitest/tree/test_display_contents.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html> + +<head> +<title>CSS display:contents tests</title> +<link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script type="application/javascript" + src="../common.js"></script> +<script type="application/javascript" + src="../role.js"></script> + +<script type="application/javascript"> +function doTest() { + let tree = + { LIST: [ + { LISTITEM: [ + { LISTITEM_MARKER: [] }, + { TEXT_LEAF: [] }, + ]}, + { LISTITEM: [ + { LISTITEM_MARKER: [] }, + { TEXT_LEAF: [] }, + ]}, + ] }; + testAccessibleTree("ul", tree); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addA11yLoadEvent(doTest); +</script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <ul id="ul" style="display: contents;"> + <li>Supermarket 1</li> + <li>Supermarket 2</li> + </ul> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_divs.html b/accessible/tests/mochitest/tree/test_divs.html new file mode 100644 index 0000000000..a8bd1f0e78 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_divs.html @@ -0,0 +1,333 @@ +<!DOCTYPE html> +<html> + +<head> +<title>div element creation tests</title> +<link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script type="application/javascript" + src="../common.js"></script> +<script type="application/javascript" + src="../role.js"></script> +<script type="application/javascript" + src="../attributes.js"></script> + +<script type="application/javascript"> +function doTest() { + // All below test cases are wrapped in a div which always gets rendered. + // c1 through c10 are the containers, the actual test cases are inside. + + // c1: Expose the div with text content + let tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // inner div + children: [ + { TEXT_LEAF: [] }, + ], // end children inner div + }, // end inner div + ], // end children outer div + }; + testAccessibleTree("c1", tree); + + // c2: Only the outermost and innermost divs are exposed. + // The middle one is skipped. This is identical to the above tree. + testAccessibleTree("c2", tree); + + // c3: Make sure the inner div with ID is exposed, but the middle one is + // skipped. + tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // inner div + attributes: { id: "b" }, + children: [ + { TEXT_LEAF: [] }, + ], // end children inner div + }, // end inner div + ], // end children outer div + }; + testAccessibleTree("c3", tree); + + // c4: Expose all three divs including the middle one due to its ID. + tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // middle div + attributes: { id: "a" }, + children: [ + { role: ROLE_SECTION, // inner div + attributes: { id: "b" }, + children: [ + { TEXT_LEAF: [] }, + ], // end children inner div + }, // end inner div + ], // end children middle div + }, // end middle div + ], // end children outer div + }; + testAccessibleTree("c4", tree); + + // c5: Expose the inner div with class b due to its text contents. + tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // inner div with class and text + attributes: { class: "b" }, + children: [ + { TEXT_LEAF: [] }, + ], // end children inner div + }, // end inner div + ], // end children outer div + }; + testAccessibleTree("c5", tree); + + // c6: Expose the outer div due to its ID, and the two inner divs due to + // their text contents. Skip the middle one. + tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // first inner div + children: [ + { TEXT_LEAF: [] }, + ], // end children first inner div + }, // end first inner div + { role: ROLE_SECTION, // second inner div + children: [ + { TEXT_LEAF: [] }, + ], // end children second inner div + }, // end second inner div + ], // end children outer div + }; + testAccessibleTree("c6", tree); + + // c7: Expose all three divs including the middle one due to it being block + // breaking. + tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // middle div + children: [ + { TEXT_LEAF: [] }, // foo + { role: ROLE_SECTION, // inner div + children: [ + { TEXT_LEAF: [] }, // bar + ], // end children inner div + }, // end inner div + { TEXT_LEAF: [] }, // baz + ], // end children middle div + }, // end middle div + ], // end children outer div + }; + testAccessibleTree("c7", tree); + + // c8: Expose all divs due to them all being text block breakers. + tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // foo div + children: [ + { TEXT_LEAF: [] }, // foo + { role: ROLE_SECTION, // baz div + children: [ + { role: ROLE_SECTION, // bar div + children: [ + { TEXT_LEAF: [] }, // bar + ], // end children bar div + }, // end bar div + { TEXT_LEAF: [] }, // baz + ], // end children baz div + }, // end baz div + ], // end children foo div + }, // end foo div + ], // end children outer div + }; + testAccessibleTree("c8", tree); + + // c9: The same, but in a different nesting order. + tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // c div + children: [ + { role: ROLE_SECTION, // b div + children: [ + { role: ROLE_SECTION, // a div + children: [ + { TEXT_LEAF: [] }, // a + ], // end children a div + }, // end a div + { TEXT_LEAF: [] }, // b + ], // end children b div + }, // end b div + { TEXT_LEAF: [] }, // c + ], // end children c div + }, // end foo div + ], // end children outer div + }; + testAccessibleTree("c9", tree); + + // c10: Both inner divs must be exposed so there is a break after b. + tree = + { role: ROLE_SECTION, // outer div with ID + children: [ + { role: ROLE_SECTION, // first inner div + children: [ + { TEXT_LEAF: [] }, // a + { TEXT_LEAF: [] }, // b + ], // end children first inner div + }, // end first inner div + { role: ROLE_SECTION, // second inner div + children: [ + { TEXT_LEAF: [] }, // c + { TEXT_LEAF: [] }, // d + ], // end children second inner div + }, // end second inner div + ], // end children outer div + }; + testAccessibleTree("c10", tree); + + // c11: A div must be exposed if it contains a br element. + tree = + { role: ROLE_SECTION, // outer div + children: [ + { role: ROLE_SECTION, // First line + children: [ + { TEXT_LEAF: [] }, // text + ], // end children first line + }, // end first line + { role: ROLE_SECTION, // Second line + children: [ + { WHITESPACE: [] }, // whitespace + ], // end children second line + }, // end second line + { role: ROLE_SECTION, // Third line + children: [ + { TEXT_LEAF: [] }, // text + ], // end children third line + }, // end third line + ], // end children outer div + }; + testAccessibleTree("c11", tree); + + // c12: Div shouldn't be rendered if first/last child text node is invisible. + tree = + { role: ROLE_SECTION, // outer div + children: [ + { role: ROLE_PARAGRAPH, + children: [ + { TEXT_LEAF: [] }, // text + ], + }, + ], // end children outer div + }; + testAccessibleTree("c12", tree); + + // c13: Div should be rendered if there is an inline frame after/before + // invisible text nodes. + tree = + { role: ROLE_SECTION, // outer div + children: [ + { TEXT_LEAF: [] }, // l1 + { role: ROLE_SECTION, // l2 + children: [ + { TEXT_LEAF: [] }, // l2 + ], // end children l2 + }, + ], // end children outer div + }; + testAccessibleTree("c13", tree); + + // c14: Div should be rendered if it contains an inline-block. + tree = + { role: ROLE_SECTION, // outer div + children: [ + { TEXT_LEAF: [] }, // l1 + { role: ROLE_SECTION, // l2 + children: [ + { role: ROLE_PUSHBUTTON, + children: [ + { TEXT_LEAF: [] }, + ], + }, + ], // end children l2 + }, + ], // end children outer div + }; + testAccessibleTree("c14", tree); + + // c15: Div should be rendered if previous sibling is text. + tree = + { role: ROLE_SECTION, // outer div + children: [ + { TEXT_LEAF: [] }, // l1 + { SECTION: [] }, // Block break + { TEXT_LEAF: [] }, // l2 + ], // end children outer div + }; + testAccessibleTree("c15", tree); + + SimpleTest.finish(); +} + +SimpleTest.waitForExplicitFinish(); +addA11yLoadEvent(doTest); +</script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Expose the div if it has plain text contents --> + <div id="c1"><div>foo</div></div> + + <!-- Expose the outer and inner div, skip the middle one. --> + <div id="c2"><div><div>foo</div></div></div> + + <!-- Expose the outer and inner divs due to the ID, but skip the middle one. --> + <div id="c3"><div><div id="b">foo</div></div></div> + + <!-- Expose all three divs and their IDs. --> + <div id="c4"><div id="a"><div id="b">foo</div></div></div> + + <!-- Expose outer and inner divs, due to text content, not class. --> + <div id="c5"><div class="a"><div class="b">foo</div></div></div> + + <!-- Expose the outer and two inner divs, skip the single middle one. --> + <div id="c6"><div><div>foo</div><div>bar</div></div></div> + + <!-- Expose all divs due to the middle one being block breaking. --> + <div id="c7"><div>foo<div>bar</div>baz</div></div> + + <!-- Expose all divs due to them all being text block breakers. --> + <div id="c8"><div>foo<div><div>bar</div>baz</div></div></div> + <div id="c9"><div><div><div>a</div>b</div>c</div></div> + + <!-- Both inner divs need to be rendered so there is a break after b. --> + <div id="c10"><div><b>a</b>b</div><div><b>c</b><b>d</b></div></div> + + <!-- Div must be rendered if it contains a br --> + <div id="c11"><div>first line.</div><div><br /></div><div>third line</div></div> + + <!-- Inner div shouldn't be rendered because although its first and last + children are text nodes, they are invisible. + --> + <div id="c12"><div> <p>Test</p> </div></div> + + <!-- Inner div should be rendered because despite the first/last invisible + text nodes, there is also an inline frame. + --> + <div id="c13">l1<div> <span>l2 </span> </div></div> + + <!-- Inner div should be rendered because it contains an inline-block. --> + <div id="c14">l1<div><button>l2</button></div></div> + + <!-- Inner div should be rendered because previous sibling is text. --> + <div id="c15">l1<div></div>l2</div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_dochierarchy.html b/accessible/tests/mochitest/tree/test_dochierarchy.html new file mode 100644 index 0000000000..4822cecb84 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_dochierarchy.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test document hierarchy</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function doTest() { + // tabDoc and testDoc are different documents depending on whether test + // is running in standalone mode or not. + + var root = getRootAccessible(); + var tabDoc = window.parent ? + getAccessible(window.parent.document, [nsIAccessibleDocument]) : + getAccessible(document, [nsIAccessibleDocument]); + var testDoc = getAccessible(document, [nsIAccessibleDocument]); + var iframeDoc = getAccessible("iframe").firstChild. + QueryInterface(nsIAccessibleDocument); + + is(root.parentDocument, null, + "Wrong parent document of root accessible"); + ok(root.childDocumentCount >= 1, + "Wrong child document count of root accessible"); + + var tabDocumentFound = false; + for (var i = 0; i < root.childDocumentCount && !tabDocumentFound; i++) { + tabDocumentFound = root.getChildDocumentAt(i) == tabDoc; + } + ok(tabDocumentFound, + "Tab document not found in children of the root accessible"); + + is(tabDoc.parentDocument, root, + "Wrong parent document of tab document"); + is(tabDoc.childDocumentCount, 1, + "Wrong child document count of tab document"); + is(tabDoc.getChildDocumentAt(0), (tabDoc == testDoc ? iframeDoc : testDoc), + "Wrong child document at index 0 of tab document"); + + if (tabDoc != testDoc) { + is(testDoc.parentDocument, tabDoc, + "Wrong parent document of test document"); + is(testDoc.childDocumentCount, 1, + "Wrong child document count of test document"); + is(testDoc.getChildDocumentAt(0), iframeDoc, + "Wrong child document at index 0 of test document"); + } + + is(iframeDoc.parentDocument, (tabDoc == testDoc ? tabDoc : testDoc), + "Wrong parent document of iframe document"); + is(iframeDoc.childDocumentCount, 0, + "Wrong child document count of iframe document"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=592913" + title="Provide a way to quickly determine whether an accessible object is a descendant of a tab document"> + Mozilla Bug 592913 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <iframe src="about:mozilla" id="iframe"></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_dockids.html b/accessible/tests/mochitest/tree/test_dockids.html new file mode 100644 index 0000000000..b19a68624a --- /dev/null +++ b/accessible/tests/mochitest/tree/test_dockids.html @@ -0,0 +1,62 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test document hierarchy</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + // enableLogging("tree,verbose"); + function doTest() { + var tree = + { DOCUMENT: [ + { TEXT_CONTAINER: [ // head + { TEXT_CONTAINER: [ // link + { STATICTEXT: [] }, // generated content + { STATICTEXT: [] }, // generated content + ] }, + ] }, + { TEXT_LEAF: [ ] }, // body text + { ENTRY: [ ] }, // input under document element + { TEXT_CONTAINER: [ // link under document element + { TEXT_LEAF: [ ] }, // link content + { STATICTEXT: [ ] }, // generated content + { STATICTEXT: [ ] }, // generated content + ] }, + { LINK: [ // anchor under document element + { TEXT_LEAF: [ ] }, // anchor content + ] }, + ] }; + testAccessibleTree(getNode("iframe").contentDocument, tree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=608887" + title="Elements appended outside the body aren't accessible"> + Mozilla Bug 608887 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <iframe src="dockids.html" id="iframe"></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_filectrl.html b/accessible/tests/mochitest/tree/test_filectrl.html new file mode 100644 index 0000000000..f0cd5baa5b --- /dev/null +++ b/accessible/tests/mochitest/tree/test_filectrl.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=483573 +--> +<head> + <title>File Input Control tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + var accTree = { + role: ROLE_GROUPING, + children: [ + { + role: ROLE_PUSHBUTTON, + }, + { + role: ROLE_LABEL, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }, + ], + }; + testAccessibleTree("filectrl", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Expose HTML5 video and audio elements' embedded controls through accessibility APIs" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=483573">Mozilla Bug 483573</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input type="file" id="filectrl" /> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_formctrl.html b/accessible/tests/mochitest/tree/test_formctrl.html new file mode 100644 index 0000000000..3046ec2bf7 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_formctrl.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html> + +<head> + <title>HTML form controls tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // input@type="checkbox" + var accTree = { + role: ROLE_CHECKBUTTON, + children: [ ], + }; + + testAccessibleTree("checkbox", accTree); + + // input@type="radio" + accTree = { + role: ROLE_RADIOBUTTON, + children: [ ], + }; + + testAccessibleTree("radio", accTree); + + // input@type="button" and input@type="submit" + // button + accTree = { + role: ROLE_PUSHBUTTON, + children: [ + { + role: ROLE_TEXT_LEAF, // XXX Bug 567203 + }, + ], + }; + + testAccessibleTree("btn1", accTree); + testAccessibleTree("submit", accTree); + testAccessibleTree("btn2", accTree); + + // input@type="image" + accTree = { + role: ROLE_PUSHBUTTON, + children: [ + { + role: ROLE_STATICTEXT, + }, + ], + }; + testAccessibleTree("image_submit", accTree); + + // input@type="range" + accTree = { SLIDER: [ ] }; + testAccessibleTree("range", accTree); + + // input@type="number" + accTree = { SPINBUTTON: [ ] }; + testAccessibleTree("number", accTree); + + // output + accTree = { + role: ROLE_STATUSBAR, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }; + testAccessibleTree("output", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Fix O(n^2) access to all the children of a container" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045"> + Bug 342045 + </a> + <a target="_blank" + title="add test for role of input type='image'" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=524521"> + Bug 524521 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=558036" + title="make HTML <output> accessible"> + Bug 558036 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559764" + title="make HTML5 input@type=range element accessible"> + Bug 559764 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <input type="checkbox" id="checkbox"> + <input type="radio" id="radio"> + <input type="button" value="button" id="btn1"> + <button id="btn2">button</button> + + <input type="submit" id="submit"> + <input type="image" id="image_submit"> + <input type="range" id="range"> + <input type="number" id="number"> + <output id="output">1337</output> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_formctrl.xhtml b/accessible/tests/mochitest/tree/test_formctrl.xhtml new file mode 100644 index 0000000000..d85a97171f --- /dev/null +++ b/accessible/tests/mochitest/tree/test_formctrl.xhtml @@ -0,0 +1,129 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<!-- Firefox toolbar --> +<?xml-stylesheet href="chrome://browser/content/browser.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL checkbox and radio hierarchy tests"> + + <!-- Firefox toolbar --> + <script type="application/javascript" + src="chrome://browser/content/browser.js"/> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + // checkbox + var accTree = { + role: ROLE_CHECKBUTTON, + children: [ ] + }; + + testAccessibleTree("checkbox", accTree); + + // radiogroup + accTree = { + role: ROLE_RADIO_GROUP, + children: [ + { + role: ROLE_RADIOBUTTON, + children: [ ] + }, + { + role: ROLE_RADIOBUTTON, + children: [ ] + } + ] + }; + + testAccessibleTree("radiogroup", accTree); + + // toolbar + accTree = { + role: ROLE_TOOLBAR, + name: "My toolbar", + children: [ + { + role: ROLE_PUSHBUTTON, + name: "hello", + children: [ ] + } + ] + }; + + testAccessibleTree("toolbar", accTree); + + // toolbar + accTree = { + role: ROLE_TOOLBAR, + name: "My second toolbar", + children: [ + { + role: ROLE_PUSHBUTTON, + name: "hello", + children: [ ] + } + ] + }; + + testAccessibleTree("toolbar2", accTree); + + if (!SEAMONKEY) + testAccessibleTree("tb_customizable", { TOOLBAR: [] }); + + SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045" + title="Fix O(n^2) access to all the children of a container"> + Mozilla Bug 342045 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <checkbox id="checkbox" label="checkbox"/> + <radiogroup id="radiogroup"> + <radio label="radio1"/> + <radio label="radio2"/> + </radiogroup> + <toolbar id="toolbar" toolbarname="My toolbar"> + <toolbarbutton id="button1" label="hello"/> + </toolbar> + <toolbar id="toolbar2" toolbarname="2nd" aria-label="My second toolbar"> + <toolbarbutton id="button2" label="hello"/> + </toolbar> + + <toolbar id="tb_customizable" customizable="true"/> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/tree/test_gencontent.html b/accessible/tests/mochitest/tree/test_gencontent.html new file mode 100644 index 0000000000..3cd5e35e9a --- /dev/null +++ b/accessible/tests/mochitest/tree/test_gencontent.html @@ -0,0 +1,69 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Generated content tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style> + .gentext:before { + content: "START" + } + .gentext:after { + content: "END" + } + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // :before and :after pseudo styles + var accTree = { + role: ROLE_SECTION, + children: [ + { + role: ROLE_STATICTEXT, + name: "START", + }, + { + role: ROLE_TEXT_LEAF, + name: "MIDDLE", + }, + { + role: ROLE_STATICTEXT, + name: "END", + }, + ], + }; + + testAccessibleTree("gentext", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Clean up our tree walker" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=530081"> + Mozilla Bug 530081 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div class="gentext" id="gentext">MIDDLE</div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_groupbox.xhtml b/accessible/tests/mochitest/tree/test_groupbox.xhtml new file mode 100644 index 0000000000..e5236d713a --- /dev/null +++ b/accessible/tests/mochitest/tree/test_groupbox.xhtml @@ -0,0 +1,63 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL groupbox hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + var accTree = + { GROUPING: [ + { LABEL: [ + { TEXT_LEAF: [ ] } + ] }, + { CHECKBUTTON: [ ] } + ] }; + testAccessibleTree("groupbox", accTree); + + SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045" + title="Fix O(n^2) access to all the children of a container"> + Mozilla Bug 342045 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <groupbox id="groupbox"> + <label value="Some caption"/> + <checkbox label="some checkbox label" /> + </groupbox> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/tree/test_iframe.html b/accessible/tests/mochitest/tree/test_iframe.html new file mode 100644 index 0000000000..e0e691550b --- /dev/null +++ b/accessible/tests/mochitest/tree/test_iframe.html @@ -0,0 +1,50 @@ +<!DOCTYPE html> +<html> +<head> + <title>Outer document accessible tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + var accTree = { + role: ROLE_INTERNAL_FRAME, + children: [ + { + role: ROLE_DOCUMENT, + }, + ], + }; + + testAccessibleTree("iframe", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Fix O(n^2) access to all the children of a container" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045"> + Mozilla Bug 342045 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <iframe id="iframe" src="about:mozilla"> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_image.xhtml b/accessible/tests/mochitest/tree/test_image.xhtml new file mode 100644 index 0000000000..ee7a6eabb1 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_image.xhtml @@ -0,0 +1,58 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL textbox and textarea hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + var accTree = { + role: ROLE_GRAPHIC, + children: [] + }; + testAccessibleTree("image", accTree); + + SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1403231" + title="Remove the image XBL binding"> + Mozilla Bug 1403231 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <image id="image" src="../moz.png" tooltiptext="hello"/> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/tree/test_img.html b/accessible/tests/mochitest/tree/test_img.html new file mode 100644 index 0000000000..c2a7351a15 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_img.html @@ -0,0 +1,84 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML img tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + // gA11yEventDumpToConsole = true; + function doPreTest() { + waitForImageMap("imgmap", doTest); + } + + function doTest() { + // image map + var accTree = { + role: ROLE_IMAGE_MAP, + children: [ + { + role: ROLE_LINK, + children: [], + }, + { + role: ROLE_LINK, + children: [], + }, + ], + }; + + testAccessibleTree("imgmap", accTree); + + // img + accTree = { + role: ROLE_GRAPHIC, + children: [], + }; + + testAccessibleTree("img", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doPreTest); + </script> + +</head> +<body> + + <a target="_blank" + title="Fix O(n^2) access to all the children of a container" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045"> + Mozilla Bug 342045 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <map name="atoz_map"> + <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" alt="b" shape="rect"> + <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a" + coords="0,0,13,14" alt="a" shape="rect"> + </map> + + <img id="imgmap" width="447" height="15" + usemap="#atoz_map" + src="../letters.gif"> + + <img id="img" src="../moz.png"> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_invalid_img.xhtml b/accessible/tests/mochitest/tree/test_invalid_img.xhtml new file mode 100644 index 0000000000..3d02900cd8 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_invalid_img.xhtml @@ -0,0 +1,48 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>invalid html img</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script> + <![CDATA[ + function doTest() { + document.getElementsByTagName("img")[0].firstChild.data = "2"; + + var accTree = { + role: ROLE_GRAPHIC, + children: [], + }; + testAccessibleTree("the_img", accTree); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> +</head> +<body> + + <a target="_blank" + title="use HyperTextAccessible for invalid img" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=852129"> + Mozilla Bug 852129 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <img id="the_img">1</img> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_invalidationlist.html b/accessible/tests/mochitest/tree/test_invalidationlist.html new file mode 100644 index 0000000000..bad908f3c8 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_invalidationlist.html @@ -0,0 +1,56 @@ +<!DOCTYPE html> +<html> +<head> + <title>Test document hierarchy</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + + <script type="application/javascript"> + function doTest() { + var tree = + {SECTION: [ + { role: ROLE_PUSHBUTTON, name: "Hello" }, + { SECTION: [ + { TEXT: [ { role: ROLE_TEXT_LEAF, name: "Hello" } ] }, + { role: ROLE_TEXT_LEAF, name: "World" } + ]} + ]}; + testAccessibleTree("container", tree); + dumpTree("container"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=673757" + title="Do not process invalidation list while tree is created"> + Mozilla Bug 673757 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="container"> + <div role="button" aria-labelledby="a"></div> + <div> + <span id="a">Hello</span><span id="b">World</span> + </div> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_list.html b/accessible/tests/mochitest/tree/test_list.html new file mode 100644 index 0000000000..5e56165292 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_list.html @@ -0,0 +1,354 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML ul/li element tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function listItemTree(aBulletText, aName, aSubtree) { + var obj = { + role: ROLE_LISTITEM, + children: [ + { + role: ROLE_LISTITEM_MARKER, + name: aBulletText, + }, + { + role: ROLE_TEXT_LEAF, + name: aName, + }, + ], + }; + + if (aSubtree) + obj.children.push(aSubtree); + + return obj; + } + + function doTest() { + // list1 + var discAccTree = { + role: ROLE_LIST, + children: [ + new listItemTree(kDiscBulletText, "Oranges"), + new listItemTree(kDiscBulletText, "Apples"), + new listItemTree(kDiscBulletText, "Bananas"), + ], + }; + + testAccessibleTree("list1", discAccTree); + + // list2 + var circleAccTree = { + role: ROLE_LIST, + children: [ + new listItemTree(kCircleBulletText, "Oranges"), + new listItemTree(kCircleBulletText, "Apples"), + new listItemTree(kCircleBulletText, "Bananas"), + ], + }; + + testAccessibleTree("list2", circleAccTree); + + // list3 + var squareAccTree = { + role: ROLE_LIST, + children: [ + new listItemTree(kSquareBulletText, "Oranges"), + new listItemTree(kSquareBulletText, "Apples"), + new listItemTree(kSquareBulletText, "Bananas"), + ], + }; + + testAccessibleTree("list3", squareAccTree); + + // list4 + var nestedAccTree = { + role: ROLE_LIST, + children: [ + new listItemTree("1. ", "Oranges"), + new listItemTree("2. ", "Apples"), + new listItemTree("3. ", "Bananas", circleAccTree), + ], + }; + + testAccessibleTree("list4", nestedAccTree); + + // dl list + var tree = + { DEFINITION_LIST: [ // dl + { TERM: [ // dt + { TEXT_LEAF: [] }, + ] }, + { DEFINITION: [ // dd + { TEXT_LEAF: [] }, + ] }, + { TERM: [ // dt + { TEXT_LEAF: [] }, + ] }, + { DEFINITION: [ // dd + { TEXT_LEAF: [] }, + ] }, + ] }; + + testAccessibleTree("list5", tree); + + // dl list inside ordered list + tree = + { LIST: [ // ol + { LISTITEM: [ // li + { LISTITEM_MARKER: [ ] }, + { DEFINITION_LIST: [ // dl + { TERM: [ // dt + { TEXT_LEAF: [] }, + ] }, + { DEFINITION: [ // dd + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }, + ] }; + + testAccessibleTree("list6", tree); + + // li having no display:list-item style + tree = + { LIST: [ // ul + { LISTITEM: [ // li + { TEXT_LEAF: [] }, + ] }, + { TEXT_LEAF: [] }, + { LISTITEM: [ // li + { TEXT_LEAF: [] }, + ] }, + ] }; + testAccessibleTree("list7", tree); + + tree = + { LIST: [ // ul + { LISTITEM: [ // li + { TEXT_LEAF: [] }, + ] }, + { LISTITEM: [ // li + { TEXT_LEAF: [] }, + ] }, + ] }; + testAccessibleTree("list8", tree); + + // span having display:list-item style + testAccessibleTree("list9", discAccTree); + + // dl with div grouping dt/dd + tree = + { DEFINITION_LIST: [ // dl + { TERM: [ // dt + { TEXT_LEAF: [] }, + ] }, + { DEFINITION: [ // dd + { TEXT_LEAF: [] }, + ] }, + { TERM: [ // dt + { TEXT_LEAF: [] }, + ] }, + { DEFINITION: [ // dd + { TEXT_LEAF: [] }, + ] }, + ] }; + + testAccessibleTree("list10", tree); + + // list-style-image + testAccessibleTree("list11", discAccTree); + + // list-style: none + tree = + { LIST: [ // ul + { LISTITEM: [ // li + { TEXT_LEAF: [] }, + ] }, + { LISTITEM: [ // li + { TEXT_LEAF: [] }, + ] }, + ] }; + testAccessibleTree("list12", tree); + + // ::marker with content + tree = { // ol + role: ROLE_LIST, + children: [ + { // li + role: ROLE_LISTITEM, + children: [ + { // ::marker content text + role: ROLE_STATICTEXT, + name: "foo", + }, + { // ::marker content counter + role: ROLE_STATICTEXT, + name: "1", + }, + { + role: ROLE_TEXT_LEAF, + name: "Oranges", + }, + ], + }, + { // li + role: ROLE_LISTITEM, + children: [ + { // ::marker content text + role: ROLE_STATICTEXT, + name: "foo", + }, + { // ::marker content counter + role: ROLE_STATICTEXT, + name: "2", + }, + { + role: ROLE_TEXT_LEAF, + name: "Apples", + }, + ], + }, + ], + }; + testAccessibleTree("list13", tree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Fix O(n^2) access to all the children of a container" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=342045"> + Mozilla Bug 342045 + </a> + <a target="_blank" + title="Wrong accessible is created for HTML:li having block display style" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=507555"> + Mozilla Bug 507555 + </a> + <a target="_blank" + title="Bullets of nested not ordered lists have one and the same character." + href="https://bugzilla.mozilla.org/show_bug.cgi?id=604587"> + Mozilla Bug 604587 + </a> + <a target="_blank" + title="Fix list bullets for DL list (crash [@ nsBulletFrame::GetListItemText])" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=629114"> + Mozilla Bug 629114 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <ul id="list1"> + <li id="l1_li1">Oranges</li> + <li id="l1_li2">Apples</li> + <li id="l1_li3">Bananas</li> + </ul> + + <ul id="list2" style="list-style-type: circle"> + <li id="l2_li1">Oranges</li> + <li id="l2_li2">Apples</li> + <li id="l2_li3">Bananas</li> + </ul> + + <ul id="list3" style="list-style-type: square"> + <li id="l3_li1">Oranges</li> + <li id="l3_li2">Apples</li> + <li id="l3_li3">Bananas</li> + </ul> + + <ol id="list4"> + <li id="li4">Oranges</li> + <li id="li5">Apples</li> + <li id="li6">Bananas<ul> + <li id="n_li4">Oranges</li> + <li id="n_li5">Apples</li> + <li id="n_li6">Bananas</li> + </ul> + </li> + </ol> + + <dl id="list5"> + <dt>item1</dt><dd>description</dd> + <dt>item2</td><dd>description</dd> + </dl> + + <ol id="list6"> + <li> + <dl id="dl"> + <dt>item1</dt><dd>description</dd> + </dl> + </li> + </ol> + + <!-- display style different than list-item --> + <ul id="list7"> + <li id="l7_li1" style="display:inline-block;">Oranges</li> + <li id="l7_li2" style="display:inline-block;">Apples</li> + </ul> + + <ul id="list8"> + <li id="l8_li1" style="display:inline; float:right;">Oranges</li> + <li id="l8_li2" style="display:inline; float:right;">Apples</li> + </ul> + + <!-- list-item display style --> + <ul id="list9"> + <span id="l9_li1" style="display:list-item">Oranges</span> + <span id="l9_li2" style="display:list-item">Apples</span> + <span id="l9_li3" style="display:list-item">Bananas</span> + </ul> + + <!-- dl with div grouping dd/dt elements (bug 1476347) --> + <dl id="list10"> + <div><dt>item1</dt><dd>description</dd></div> + <div><dt>item2</td><dd>description</dd></div> + </dl> + + <!-- list-style-image --> + <ul id="list11" + style="list-style-type: none; list-style-image: url('../moz.png');"> + <li>Oranges</li> + <li>Apples</li> + <li>Bananas</li> + </ul> + + <!-- list-style: none --> + <ul id="list12" style="list-style: none;"> + <li>Oranges</li> + <li>Apples</li> + </ul> + + <!-- ::marker with content --> + <style> + #list13 li { + counter-increment: list13counter; + } + #list13 li::marker { + content: 'foo' counter(list13counter); + } + </style> + <ol id="list13"> + <li>Oranges</li> + <li>Apples</li> + </ol> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_map.html b/accessible/tests/mochitest/tree/test_map.html new file mode 100644 index 0000000000..72de628f9f --- /dev/null +++ b/accessible/tests/mochitest/tree/test_map.html @@ -0,0 +1,81 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML map accessible tree tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // map used as imagemap, not accessible + var accTree = + { SECTION: [ ] }; + + testAccessibleTree("imagemapcontainer", accTree); + + // map group. Imagemaps are inlines by default, so TEXT_CONTAINER. + accTree = + { TEXT_CONTAINER: [ + { PARAGRAPH: [ + { TEXT_LEAF: [ ] }, + { LINK: [ + { TEXT_LEAF: [ ] }, + ] }, + { TEXT_LEAF: [ ] }, + { LINK: [ + { TEXT_LEAF: [ ] }, + ] }, + { TEXT_LEAF: [ ] }, + ] }, + ] }; + + testAccessibleTree("mapgroup", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Map used for grouping is not accessible under certain circumstances" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=627718"> + Mozilla Bug 627718 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="imagemapcontainer"> + <map name="atoz_map"> + <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#b" + coords="17,0,30,14" alt="b" shape="rect"> + <area href="http://www.bbc.co.uk/radio4/atoz/index.shtml#a" + coords="0,0,13,14" alt="a" shape="rect"> + </map> + </div> + + <img id="imgmap" width="447" height="15" + usemap="#atoz_map" + src="../letters.gif"> + + <map id="mapgroup" title="Navigation Bar" name="mapgroup"> + <p> + [<a href="#how">Bypass navigation bar</a>] + [<a href="home.html">Home</a>] + </p> + </map> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_media.html b/accessible/tests/mochitest/tree/test_media.html new file mode 100644 index 0000000000..1880f9c8fa --- /dev/null +++ b/accessible/tests/mochitest/tree/test_media.html @@ -0,0 +1,121 @@ +<!DOCTYPE html> +<html> +<!-- +https://bugzilla.mozilla.org/show_bug.cgi?id=483573 +--> +<head> + <title>HTML5 audio/video tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + + async function loadAudioSource() { + /** + * Setting the source dynamically and wait for it to load, + * so we can test the accessibility tree of the control in its ready and + * stable state. + * + * See bug 1484048 comment 25 for discussion on how it switches UI when + * loading a statically declared source. + */ + const bufferA11yShown = waitForEvent( + EVENT_SHOW, + evt => evt.accessible.role == ROLE_TEXT_LEAF && + evt.accessible.indexInParent == 2 && + evt.accessible.parent.role == ROLE_STATUSBAR + ); + await new Promise(resolve => { + let el = document.getElementById("audio"); + el.addEventListener("canplaythrough", resolve, {once: true}); + el.src = "../bug461281.ogg"; + }); + // Wait for this to be reflected in the a11y tree. + await bufferA11yShown; + + doTest(); + } + + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // test the accessible tree + + var accTree = { + role: ROLE_GROUPING, + children: [ + { // start/stop button + role: ROLE_PUSHBUTTON, + name: "Play", + children: [], + }, + { // buffer bar + role: ROLE_STATUSBAR, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Loading:", + }, + { + role: ROLE_TEXT_LEAF, + name: " ", + }, + { + role: ROLE_TEXT_LEAF, + // The name is the percentage buffered; e.g. "0%", "100%". + // We can't check it here because it might be different + // depending on browser caching. + }, + ], + }, + { // slider of progress bar + role: ROLE_SLIDER, + name: "Position", + value: "0:00 / 0:02", + children: [], + }, + { // mute button + role: ROLE_PUSHBUTTON, + name: "Mute", + children: [], + }, + { // slider of volume bar + role: ROLE_SLIDER, + children: [], + name: "Volume", + }, + ], + }; + testAccessibleTree("audio", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(loadAudioSource); + </script> +</head> +<body> + + <a target="_blank" + title="Expose HTML5 video and audio elements' embedded controls through accessibility APIs" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=483573">Mozilla Bug 483573</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <audio id="audio" controls="true"></audio> + + <div id="eventDump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_select.html b/accessible/tests/mochitest/tree/test_select.html new file mode 100644 index 0000000000..4f9fb35ecf --- /dev/null +++ b/accessible/tests/mochitest/tree/test_select.html @@ -0,0 +1,137 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML select control tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + var accTree = { + role: ROLE_LISTBOX, + children: [ + { + role: ROLE_GROUPING, + children: [ + { + role: ROLE_STATICTEXT, + children: [ ], + }, + { + role: ROLE_OPTION, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }, + { + role: ROLE_OPTION, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }, + ], + }, + { + role: ROLE_OPTION, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }, + ], + }; + testAccessibleTree("listbox", accTree); + + accTree = { + role: ROLE_COMBOBOX, + children: [ + { + role: ROLE_COMBOBOX_LIST, + children: [ + { + role: ROLE_GROUPING, + children: [ + { + role: ROLE_STATICTEXT, + children: [ ], + }, + { + role: ROLE_COMBOBOX_OPTION, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }, + { + role: ROLE_COMBOBOX_OPTION, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }, + ], + }, + { + role: ROLE_COMBOBOX_OPTION, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }, + ], + }, + ], + }; + testAccessibleTree("combobox", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="remove all the code in #ifdef COMBO_BOX_WITH_THREE_CHILDREN" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=506616"> + Mozilla Bug 506616 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="listbox" size="4"> + <optgroup label="Colors"> + <option>Red</option> + <option>Blue</option> + </optgroup> + <option>Animal</option> + </select> + + <select id="combobox"> + <optgroup label="Colors"> + <option>Red</option> + <option>Blue</option> + </optgroup> + <option>Animal</option> + </select> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_svg.html b/accessible/tests/mochitest/tree/test_svg.html new file mode 100644 index 0000000000..4a071d0f4a --- /dev/null +++ b/accessible/tests/mochitest/tree/test_svg.html @@ -0,0 +1,127 @@ +<!DOCTYPE html> +<html> +<head> + <title>SVG Tree Tests</title> + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // svgText + var accTree = { + role: ROLE_DIAGRAM, + children: [ + { + role: ROLE_TEXT_CONTAINER, + children: [ + { + role: ROLE_TEXT_LEAF, + }, + ], + }, + ], + }; + testAccessibleTree("svgText", accTree); + + // svg1 + accTree = { + role: ROLE_DIAGRAM, + children: [] + }; + testAccessibleTree("svg1", accTree); + + // svg2 + accTree = { + role: ROLE_DIAGRAM, + children: [ + { + role: ROLE_GROUPING, + children: [] + } + ] + }; + testAccessibleTree("svg2", accTree); + + // svg3 + accTree = { + role: ROLE_DIAGRAM, + children: [ + { + role: ROLE_GRAPHIC, + children: [] + } + ] + }; + testAccessibleTree("svg3", accTree); + + // svg4 + accTree = { + role: ROLE_DIAGRAM, + children: [ + { + role: ROLE_GROUPING, + children: [ + { + role: ROLE_GRAPHIC, + children: [] + } + ] + } + ] + }; + testAccessibleTree("svg4", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <svg id="svgText"> + <text>This is some text</text> + </svg> + + <!-- no accessible objects --> + <svg id="svg1"> + <g id="g1"> + <rect width="300" height="100" id="rect1" style="fill:#00f" /> + </g> + </svg> + + <svg id="svg2"> + <g id="g2"> + <title>g</title> + <rect width="300" height="100" id="rect2" style="fill:#00f" /> + </g> + </svg> + + <svg id="svg3"> + <g id="g3"> + <rect width="300" height="100" id="rect3" style="fill:#00f"> + <title>rect</title> + </rect> + </g> + </svg> + + <svg id="svg4"> + <g id="g4"> + <title>g</title> + <rect width="300" height="100" id="rect4" style="fill:#00f"> + <title>rect</title> + </rect> + </g> + </svg> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_tabbox.xhtml b/accessible/tests/mochitest/tree/test_tabbox.xhtml new file mode 100644 index 0000000000..65a4885b76 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_tabbox.xhtml @@ -0,0 +1,98 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tabbox hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // tabbox + + var accTree = { + role: ROLE_PAGETABLIST, + children: [ + { + role: ROLE_PAGETAB, + children: [] + }, + { + role: ROLE_PAGETAB, + children: [] + } + ] + }; + testAccessibleTree("tabs", accTree); + + accTree = { + role: ROLE_PANE, + children: [ + { + role: ROLE_PROPERTYPAGE, + children: [] + }, + { + role: ROLE_PROPERTYPAGE, + children: [] + } + ] + }; + testAccessibleTree("tabpanels", accTree); + + SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=540389" + title=" WARNING: Bad accessible tree!: [tabbrowser tab] "> + Mozilla Bug 540389 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=552944" + title="No relationship between tabs and associated property page in new tabbrowser construct"> + Mozilla Bug 552944 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tabbox> + <tabs id="tabs"> + <tab label="tab1"/> + <tab label="tab2"/> + </tabs> + <tabpanels id="tabpanels"> + <tabpanel/> + <tabpanel/> + </tabpanels> + </tabbox> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/tree/test_tabbrowser.xhtml b/accessible/tests/mochitest/tree/test_tabbrowser.xhtml new file mode 100644 index 0000000000..401ea1a2b1 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_tabbrowser.xhtml @@ -0,0 +1,261 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> + +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tabbrowser hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../browser.js"></script> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // invoker + function testTabHierarchy() + { + this.eventSeq = [ + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 0), + new asyncInvokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, tabDocumentAt, 1) + ]; + + this.invoke = function testTabHierarchy_invoke() + { + var docURIs = ["about:license", "about:mozilla"]; + tabBrowser().loadTabs(docURIs, { + inBackground: false, + replace: true, + triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(), + }); + // Flush layout, so as to guarantee that the a11y tree is constructed. + browserDocument().documentElement.getBoundingClientRect(); + } + + this.finalCheck = function testTabHierarchy_finalCheck(aEvent) + { + //////////////////// + // Tab bar + //////////////////// + var tabsAccTree = { + // xul:tabs + role: ROLE_PAGETABLIST, + children: [ + // Children depend on application (UI): see below. + ] + }; + + // SeaMonkey and Firefox tabbrowser UIs differ. + if (SEAMONKEY) { + SimpleTest.ok(true, "Testing SeaMonkey tabbrowser UI."); + + tabsAccTree.children.splice(0, 0, + { + // xul:toolbarbutton ("Open a new tab") + role: ROLE_PUSHBUTTON, + children: [] + }, + { + // xul:tab ("about:license") + role: ROLE_PAGETAB, + children: [] + }, + { + // tab ("about:mozilla") + role: ROLE_PAGETAB, + children: [] + }, + { + // xul:toolbarbutton ("List all tabs") + role: ROLE_PUSHBUTTON, + children: [ + { + // xul:menupopup + role: ROLE_MENUPOPUP, + children: [] + } + ] + }, + { + // xul:toolbarbutton ("Close current tab") + role: ROLE_PUSHBUTTON, + children: [] + } + ); + } else { + SimpleTest.ok(true, "Testing Firefox tabbrowser UI."); + let newTabChildren = []; + if (SpecialPowers.getBoolPref("privacy.userContext.enabled")) { + newTabChildren = [ + { + role: ROLE_MENUPOPUP, + children: [] + } + ]; + } + + // NB: The (3) buttons are not visible, unless manually hovered, + // probably due to size reduction in this test. + tabsAccTree.children.splice(0, 0, + { + // xul:tab ("about:license") + role: ROLE_PAGETAB, + children: [ + { + // xul:text, i.e. the tab label text + role: ROLE_TEXT_LEAF, + children: [] + } + ] + }, + { + // tab ("about:mozilla") + role: ROLE_PAGETAB, + children: [ + { + // xul:text, i.e. the tab label text + role: ROLE_TEXT_LEAF, + children: [] + } + ] + }, + { + // xul:toolbarbutton ("Open a new tab") + role: ROLE_PUSHBUTTON, + children: newTabChildren + } + // "List all tabs" dropdown + // XXX: This child(?) is not present in this test. + // I'm not sure why (though probably expected). + ); + } + + testAccessibleTree(tabBrowser().tabContainer, tabsAccTree); + + //////////////////// + // Tab contents + //////////////////// + var tabboxAccTree = { + // xul:tabpanels + role: ROLE_PANE, + children: [ + { + // xul:notificationbox + role: ROLE_PROPERTYPAGE, + children: [ + { + // xul:browser + role: ROLE_INTERNAL_FRAME, + children: [ + { + // #document ("about:license") + role: ROLE_DOCUMENT + // children: [ ... ] // Ignore document content. + } + ] + } + ] + }, + { + // notificationbox + role: ROLE_PROPERTYPAGE, + children: [ + { + // browser + role: ROLE_INTERNAL_FRAME, + children: [ + { + // #document ("about:mozilla") + role: ROLE_DOCUMENT + // children: [ ... ] // Ignore document content. + } + ] + } + ] + }, + { + // notificationbox + role: ROLE_PROPERTYPAGE, + children: [ + { + // browser + role: ROLE_INTERNAL_FRAME, + children: [ + { + // #document ("about:newtab" preloaded) + role: ROLE_DOCUMENT + // children: [ ... ] // Ignore document content. + } + ] + } + ] + } + ] + }; + + testAccessibleTree(tabBrowser().tabbox.tabpanels, tabboxAccTree); + } + + this.getID = function testTabHierarchy_getID() + { + return "hierarchy of tabs"; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + gA11yEventDumpToConsole = true; + //enableLogging("tree,verbose,stack"); + + var gQueue = null; + function doTest() + { + SimpleTest.requestCompleteLog(); + + // Load documents into tabs and wait for docLoadComplete events caused by these + // documents load before we start the test. + gQueue = new eventQueue(); + + gQueue.push(new testTabHierarchy()); + gQueue.onFinish = function() { closeBrowserWindow(); } + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + openBrowserWindow(doTest); + ]]> + </script> + + <vbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=540389" + title=" WARNING: Bad accessible tree!: [tabbrowser tab] "> + Mozilla Bug 540389 + </a><br/> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=552944" + title="No relationship between tabs and associated property page in new tabbrowser construct"> + Mozilla Bug 552944 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox id="eventdump"></vbox> + </vbox> + +</window> diff --git a/accessible/tests/mochitest/tree/test_table.html b/accessible/tests/mochitest/tree/test_table.html new file mode 100644 index 0000000000..492112b003 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_table.html @@ -0,0 +1,458 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML table tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // //////////////////////////////////////////////////////////////////////// + // tables having captions + + // Two captions, first is used, second is ignored. + var accTree = + { TABLE: [ + { CAPTION: [ + { + role: ROLE_TEXT_LEAF, + name: "caption", + }, + ] }, + { ROW: [ + { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] }, + { COLUMNHEADER: [ { TEXT_LEAF: [ ] } ] }, + ] }, + { ROW: [ + { CELL: [ { TEXT_LEAF: [ ] } ] }, + { CELL: [ { TEXT_LEAF: [ ] } ] }, + ] }, + { ROW: [ + { CELL: [ { TEXT_LEAF: [ ] } ] }, + { CELL: [ { TEXT_LEAF: [ ] } ] }, + ] }, + { ROW: [ + { CELL: [ { TEXT_LEAF: [ ] } ] }, + { CELL: [ { TEXT_LEAF: [ ] } ] }, + ] }, + ] }; + + testAccessibleTree("table", accTree); + + // One caption, empty text, caption is included. + accTree = + { TABLE: [ + { CAPTION: [ ] }, + { ROW: [ + { CELL: [ { TEXT_LEAF: [ ] } ] }, + { CELL: [ { TEXT_LEAF: [ ] } ] }, + ] }, + ] }; + + testAccessibleTree("table_caption_empty", accTree); + + // Two captions, first has empty text, second is ignored. + accTree = + { TABLE: [ + { CAPTION: [ ] }, + { ROW: [ + { CELL: [ { TEXT_LEAF: [ ] } ] }, + { CELL: [ { TEXT_LEAF: [ ] } ] }, + ] }, + ] }; + + testAccessibleTree("table_caption_firstempty", accTree); + + // One caption, placed in the end of table. In use. + accTree = + { TABLE: [ + { CAPTION: [ + { + role: ROLE_TEXT_LEAF, + name: "caption", + }, + ] }, + { ROW: [ + { CELL: [ { TEXT_LEAF: [ ] } ] }, + { CELL: [ { TEXT_LEAF: [ ] } ] }, + ] }, + ] }; + + testAccessibleTree("table_caption_intheend", accTree); + + // One caption, collapsed to zero width and height. In use. + accTree = + { TABLE: [ + { CAPTION: [ + { + role: ROLE_TEXT_LEAF, + name: "caption", + }, + ] }, + { ROW: [ + { CELL: [ { TEXT_LEAF: [ ] } ] }, + { CELL: [ { TEXT_LEAF: [ ] } ] }, + ] }, + ] }; + + testAccessibleTree("table_caption_collapsed", accTree); + + // //////////////////////////////////////////////////////////////////////// + // table2 (consist of one column) + + accTree = { + role: ROLE_TABLE, + children: [ + { + role: ROLE_ROW, + children: [ + { + role: ROLE_COLUMNHEADER, + }, + ], + }, + { + role: ROLE_ROW, + children: [ + { + role: ROLE_CELL, + }, + ], + }, + ], + }; + + testAccessibleTree("table2", accTree); + + // //////////////////////////////////////////////////////////////////////// + // table3 (consist of one row) + + accTree = { + role: ROLE_TABLE, + children: [ + { + role: ROLE_ROW, + children: [ + { + role: ROLE_ROWHEADER, + }, + { + role: ROLE_CELL, + }, + ], + }, + ], + }; + + testAccessibleTree("table3", accTree); + + // /////////////////////////////////////////////////////////////////////// + // table4 (display: table-row) + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] } ], + }; + testAccessibleTree("table4", accTree); + + // /////////////////////////////////////////////////////////////////////// + // table5 (tbody with display: block should not get accessible) + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree("table5", accTree); + + // /////////////////////////////////////////////////////////////////////// + // log table + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree("logtable", accTree); + + // /////////////////////////////////////////////////////////////////////// + // display:block table + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree("block_table", accTree); + + // /////////////////////////////////////////////////////////////////////// + // display:inline table + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree("inline_table1", accTree); + + // /////////////////////////////////////////////////////////////////////// + // display:inline table + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }, + ] }, + ] }, + ] }; + testAccessibleTree("table_containing_inlinetable", accTree); + + // /////////////////////////////////////////////////////////////////////// + // table with a cell that has display:block + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree("table_containing_block_cell", accTree); + + // /////////////////////////////////////////////////////////////////////// + // A table with all elements being display:block, including a row group. + // This makes us fall back to the ARIAGridRowAccessible. + // Strange example from Gmail. + accTree = + { TABLE: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree("table_all_display_block", accTree); + + // /////////////////////////////////////////////////////////////////////// + // A table with a display:block tbody that has an aria role + // The tbody should get an accessible with the desired role. + accTree = + { TABLE: [ + { DIALOG: [ + { TEXT_CONTAINER: [ + { TEXT_CONTAINER: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }, + ] }; + testAccessibleTree("table_with_block_tbody_and_role", accTree); + + // /////////////////////////////////////////////////////////////////////// + // A table with a display:block tbody that is focusable + // The tbody should get a grouping accessible. + accTree = + { TABLE: [ + { GROUPING: [ + { ROW: [ + { CELL: [ + { TEXT_LEAF: [ ] }, + ] }, + ] }, + ] }, + ] }; + testAccessibleTree("table_with_focusable_block_tbody", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="When a table has only one column per row and that column happens to be a column header its role is exposed wrong" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=529621"> + Mozilla Bug 529621 + </a> + <a target="_blank" + title="when div has display style table-row" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=727722"> + Mozilla Bug 727722 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table id="table"> + <thead> + <tr> + <th>col1</th><th>col2</th> + </tr> + </thead> + <caption>caption</caption> + <tbody> + <tr> + <td>cell1</td><td>cell2</td> + </tr> + </tbody> + <tr> + <td>cell3</td><td>cell4</td> + </tr> + <caption>caption2</caption> + <tfoot> + <tr> + <td>cell5</td><td>cell6</td> + </tr> + </tfoot> + </table> + + <table id="table_caption_empty"> + <caption></caption> + <tr> + <td>cell1</td><td>cell2</td> + </tr> + </table> + + <table id="table_caption_firstempty"> + <caption></caption> + <tr> + <td>cell1</td><td>cell2</td> + </tr> + <caption>caption</caption> + </table> + + <table id="table_caption_intheend"> + <tr> + <td>cell1</td><td>cell2</td> + </tr> + <caption>caption</caption> + </table> + + <table id="table_caption_collapsed"> + <caption style="width: 0; height: 0">caption</caption> + <tr> + <td>cell1</td><td>cell2</td> + </tr> + </table> + <table id="table2"> + <thead> + <tr> + <th>colheader</th> + </tr> + </thead> + <tbody> + <tr> + <td>bla</td> + </tr> + </tbody> + </table> + + <table id="table3"> + <tr> + <th>rowheader</th> + <td>cell</td> + </tr> + </table> + + <table id="table4"> + <div style="display: table-row"> + <td>cell1</td> + </div> + </table> + + <table id="table5"> + <tbody style="display:block;overflow:auto;"> + <tr> + <td>bla</td> + </tr> + </tbody> + </table> + + <table id="logtable" role="log"><tr><td>blah</td></tr></table> + + <table id="block_table" style="display: block;"> + <tr> + <td>bla</td> + </tr> + </table> + + <table id="inline_table1" border="1" style="display:inline"> + <tr> + <td>table1 cell1</td> + <td>table1 cell2</td> + </tr> + </table> + + <table id="table_containing_inlinetable"><tr><td> + <table id="inline_table2" border="1" style="display:inline"> + <tr id="tr_in_inline_table2"> + <td id="td_in_inline_table2">cell</td> + </tr> + </table> + </td></tr></table> + + <table id="table_containing_block_cell"> + <tr> + <td>Normal cell</td> + <td style="display: block;">Block cell</td> + </tr> + </table> + <table id="table_all_display_block" style="display:block;"> + <tbody style="display:block;"> + <tr style="display:block;"> + <td style="display:block;">text</td> + </tr> + </tbody> + </table> + + <table id="table_with_block_tbody_and_role"> + <tbody style="display:block;" role="dialog"> + <tr> + <td>text</td> + </tr> + </tbody> + </table> + + <table id="table_with_focusable_block_tbody"> + <tbody style="display:block;" tabindex="0"> + <tr> + <td>text</td> + </tr> + </tbody> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_table_2.html b/accessible/tests/mochitest/tree/test_table_2.html new file mode 100644 index 0000000000..5d0aedf2b7 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_table_2.html @@ -0,0 +1,240 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + +<style> +.responsive-table { + width: 100%; + margin-bottom: 1.5em; +} +.responsive-table thead { + position: absolute; + clip: rect(1px 1px 1px 1px); + /* IE6, IE7 */ + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} +.responsive-table thead th { + background-color: #1d96b2; + border: 1px solid #1d96b2; + font-weight: normal; + text-align: center; + color: white; +} +.responsive-table thead th:first-of-type { + text-align: left; +} +.responsive-table tbody, +.responsive-table tr, +.responsive-table th, +.responsive-table td { + display: block; + padding: 0; + text-align: left; + white-space: normal; +} +.responsive-table th, +.responsive-table td { + padding: .5em; + vertical-align: middle; +} +.responsive-table caption { + margin-bottom: 1em; + font-size: 1em; + font-weight: bold; + text-align: center; +} +.responsive-table tfoot { + font-size: .8em; + font-style: italic; +} +.responsive-table tbody tr { + margin-bottom: 1em; + border: 2px solid #1d96b2; +} +.responsive-table tbody tr:last-of-type { + margin-bottom: 0; +} +.responsive-table tbody th[scope="row"] { + background-color: #1d96b2; + color: white; +} +.responsive-table tbody td[data-type=currency] { + text-align: right; +} +.responsive-table tbody td[data-title]:before { + content: attr(data-title); + float: left; + font-size: .8em; + color: rgba(94, 93, 82, 0.75); +} +.responsive-table tbody td { + text-align: right; + border-bottom: 1px solid #1d96b2; +} + +.responsive-table { + font-size: .9em; +} +.responsive-table thead { + position: relative; + clip: auto; + height: auto; + width: auto; + overflow: auto; +} +.responsive-table tr { + display: table-row; +} +.responsive-table th, +.responsive-table td { + display: table-cell; + padding: .5em; +} + +.responsive-table caption { + font-size: 1.5em; +} +.responsive-table tbody { + display: table-row-group; +} +.responsive-table tbody tr { + display: table-row; + border-width: 1px; +} +.responsive-table tbody tr:nth-of-type(even) { + background-color: rgba(94, 93, 82, 0.1); +} +.responsive-table tbody th[scope="row"] { + background-color: transparent; + color: #5e5d52; + text-align: left; +} +.responsive-table tbody td { + text-align: center; +} +.responsive-table tbody td[data-title]:before { + content: none; +} +</style> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script type="application/javascript" + src="../common.js"></script> +<script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../table.js"></script> + +<script type="application/javascript"> + +const COLHEADER = ROLE_COLUMNHEADER; +const ROWHEADER = ROLE_ROWHEADER; +const CELL = ROLE_CELL; +const TEXT_LEAF = ROLE_TEXT_LEAF; + +function doTest() { + let accTree = + { TABLE: [ + { CAPTION: [ + { + role: ROLE_TEXT_LEAF, + name: "Top 10 Grossing Animated Films of All Time", + }, + ] }, + { ROW: [ + { role: COLHEADER, name: "Film Title" }, + { role: COLHEADER, name: "Released" }, + { role: COLHEADER, name: "Studio" }, + { role: COLHEADER, name: "Worldwide Gross" }, + { role: COLHEADER, name: "Domestic Gross" }, + { role: COLHEADER, name: "Foreign Gross" }, + { role: COLHEADER, name: "Budget" }, + ] }, + { ROW: [ + { role: CELL }, + ] }, + { ROW: [ + { role: ROWHEADER, name: "Toy Story 3" }, + { CELL: [ { role: TEXT_LEAF, name: "2010" }] }, + { CELL: [ { role: TEXT_LEAF, name: "Disney Pixar" }] }, + { CELL: [ { role: TEXT_LEAF, name: "$1,063,171,911" }] }, + { CELL: [ { role: TEXT_LEAF, name: "$415,004,880" }] }, + { CELL: [ { role: TEXT_LEAF, name: "$648,167,031" }] }, + { CELL: [ { role: TEXT_LEAF, name: "$200,000,000" }] }, + ] }, + { ROW: [ + { role: ROWHEADER, name: "Shrek Forever After" }, + { CELL: [ { role: TEXT_LEAF, name: "2010" }] }, + { CELL: [ { role: TEXT_LEAF, name: "Dreamworks" }] }, + { CELL: [ { role: TEXT_LEAF, name: "$752,600,867" }] }, + { CELL: [ { role: TEXT_LEAF, name: "$238,736,787" }] }, + { CELL: [ { role: TEXT_LEAF, name: "$513,864,080" }] }, + { CELL: [ { role: TEXT_LEAF, name: "$165,000,000" }] }, + ] }, + ] }; + + testAccessibleTree("table", accTree); + + SimpleTest.finish(); +} +SimpleTest.waitForExplicitFinish(); +addA11yLoadEvent(doTest); +</script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table class="responsive-table" id="table"> + <caption>Top 10 Grossing Animated Films of All Time</caption> + <thead> + <tr> + <th scope="col">Film Title</th> + <th scope="col">Released</th> + <th scope="col">Studio</th> + <th scope="col">Worldwide Gross</th> + <th scope="col">Domestic Gross</th> + <th scope="col">Foreign Gross</th> + <th scope="col">Budget</th> + </tr> + </thead> + <tfoot> + <tr> + <td colspan="7">Sources: <a href="http://en.wikipedia.org/wiki/List_of_highest-grossing_animated_films" rel="external">Wikipedia</a> & <a href="http://www.boxofficemojo.com/genres/chart/?id=animation.htm" rel="external">Box Office Mojo</a>. Data is current as of March 12, 2014</td> + </tr> + </tfoot> + <tbody> + <tr> + <th scope="row">Toy Story 3</th> + <td data-title="Released">2010</td> + <td data-title="Studio">Disney Pixar</td> + <td data-title="Worldwide Gross" data-type="currency">$1,063,171,911</td> + <td data-title="Domestic Gross" data-type="currency">$415,004,880</td> + <td data-title="Foreign Gross" data-type="currency">$648,167,031</td> + <td data-title="Budget" data-type="currency">$200,000,000</td> + </tr> + <tr> + <th scope="row">Shrek Forever After</th> + <td data-title="Released">2010</td> + <td data-title="Studio">Dreamworks</td> + <td data-title="Worldwide Gross" data-type="currency">$752,600,867</td> + <td data-title="Domestic Gross" data-type="currency">$238,736,787</td> + <td data-title="Foreign Gross" data-type="currency">$513,864,080</td> + <td data-title="Budget" data-type="currency">$165,000,000</td> + </tr> + </tbody> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_table_3.html b/accessible/tests/mochitest/tree/test_table_3.html new file mode 100644 index 0000000000..73d7b54f58 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_table_3.html @@ -0,0 +1,242 @@ +<!DOCTYPE HTML PUBLIC "-//w3c//dtd html 4.0 transitional//en"> +<html> +<head> +<meta http-equiv="content-type" content="text/html; charset=UTF-8"> +<link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + +<style> +.responsive-table { + width: 100%; + margin-bottom: 1.5em; +} +.responsive-table thead { + position: absolute; + clip: rect(1px 1px 1px 1px); + /* IE6, IE7 */ + clip: rect(1px, 1px, 1px, 1px); + padding: 0; + border: 0; + height: 1px; + width: 1px; + overflow: hidden; +} +.responsive-table thead th { + background-color: #1d96b2; + border: 1px solid #1d96b2; + font-weight: normal; + text-align: center; + color: white; +} +.responsive-table thead th:first-of-type { + text-align: left; +} +.responsive-table tbody, +.responsive-table tr, +.responsive-table th, +.responsive-table td { + display: block; + padding: 0; + text-align: left; + white-space: normal; +} +.responsive-table th, +.responsive-table td { + padding: .5em; + vertical-align: middle; +} +.responsive-table caption { + margin-bottom: 1em; + font-size: 1em; + font-weight: bold; + text-align: center; +} +.responsive-table tfoot { + font-size: .8em; + font-style: italic; +} +.responsive-table tbody tr { + margin-bottom: 1em; + border: 2px solid #1d96b2; +} +.responsive-table tbody tr:last-of-type { + margin-bottom: 0; +} +.responsive-table tbody th[scope="row"] { + background-color: #1d96b2; + color: white; +} +.responsive-table tbody td[data-type=currency] { + text-align: right; +} +.responsive-table tbody td[data-title]:before { + content: attr(data-title); + float: left; + font-size: .8em; + color: #1d96b2; + font-weight: bold; +} +.responsive-table tbody td { + text-align: right; + border-bottom: 1px solid #1d96b2; +} + +/* float everything */ +.responsive-table tbody tr { + float: left; + width: 48%; + margin-left: 2%; +} +</style> + +<script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script type="application/javascript" + src="../common.js"></script> +<script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../table.js"></script> + +<script type="application/javascript"> + +const COLHEADER = ROLE_COLUMNHEADER; +const ROWHEADER = ROLE_ROWHEADER; +const CELL = ROLE_CELL; +const STATICTEXT = ROLE_STATICTEXT; +const TEXT_LEAF = ROLE_TEXT_LEAF; +const GROUPING = ROLE_GROUPING; + +function doTest() { + let accTree = + { TABLE: [ + { CAPTION: [ + { + role: ROLE_TEXT_LEAF, + name: "Top 10 Grossing Animated Films of All Time", + }, + ] }, + { ROW: [ + { CELL: [ { role: TEXT_LEAF, name: "Film Title" } ] }, + { CELL: [ { role: TEXT_LEAF, name: "Released" } ] }, + { CELL: [ { role: TEXT_LEAF, name: "Studio" } ] }, + { CELL: [ { role: TEXT_LEAF, name: "Worldwide Gross" } ] }, + { CELL: [ { role: TEXT_LEAF, name: "Domestic Gross" } ] }, + { CELL: [ { role: TEXT_LEAF, name: "Foreign Gross" } ] }, + { CELL: [ { role: TEXT_LEAF, name: "Budget" } ] }, + ] }, + { ROW: [ + { role: CELL }, + ] }, + { ROW: [ + { CELL: [ { role: TEXT_LEAF, name: "Toy Story 3" } ] }, + { CELL: [ + { role: STATICTEXT, name: "Released" }, + { role: TEXT_LEAF, name: "2010" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Studio" }, + { role: TEXT_LEAF, name: "Disney Pixar" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Worldwide Gross" }, + { role: TEXT_LEAF, name: "$1,063,171,911" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Domestic Gross" }, + { role: TEXT_LEAF, name: "$415,004,880" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Foreign Gross" }, + { role: TEXT_LEAF, name: "$648,167,031" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Budget" }, + { role: TEXT_LEAF, name: "$200,000,000" }, + ]}, + ] }, + { ROW: [ + { CELL: [ { role: TEXT_LEAF, name: "Shrek Forever After" } ] }, + { CELL: [ + { role: STATICTEXT, name: "Released" }, + { role: TEXT_LEAF, name: "2010" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Studio" }, + { role: TEXT_LEAF, name: "Dreamworks" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Worldwide Gross" }, + { role: TEXT_LEAF, name: "$752,600,867" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Domestic Gross" }, + { role: TEXT_LEAF, name: "$238,736,787" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Foreign Gross" }, + { role: TEXT_LEAF, name: "$513,864,080" }, + ] }, + { CELL: [ + { role: STATICTEXT, name: "Budget" }, + { role: TEXT_LEAF, name: "$165,000,000" }, + ] }, + ] }, + ] }; + + testAccessibleTree("table", accTree); + + SimpleTest.finish(); +} +SimpleTest.waitForExplicitFinish(); +addA11yLoadEvent(doTest); +</script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table class="responsive-table" id="table"> + <caption>Top 10 Grossing Animated Films of All Time</caption> + <thead> + <tr> + <th scope="col">Film Title</th> + <th scope="col">Released</th> + <th scope="col">Studio</th> + <th scope="col">Worldwide Gross</th> + <th scope="col">Domestic Gross</th> + <th scope="col">Foreign Gross</th> + <th scope="col">Budget</th> + </tr> + </thead> + <tfoot> + <tr> + <td colspan="7">Sources: <a href="http://en.wikipedia.org/wiki/List_of_highest-grossing_animated_films" rel="external">Wikipedia</a> & <a href="http://www.boxofficemojo.com/genres/chart/?id=animation.htm" rel="external">Box Office Mojo</a>. Data is current as of March 12, 2014</td> + </tr> + </tfoot> + <tbody> + <tr> + <th scope="row">Toy Story 3</th> + <td data-title="Released">2010</td> + <td data-title="Studio">Disney Pixar</td> + <td data-title="Worldwide Gross" data-type="currency">$1,063,171,911</td> + <td data-title="Domestic Gross" data-type="currency">$415,004,880</td> + <td data-title="Foreign Gross" data-type="currency">$648,167,031</td> + <td data-title="Budget" data-type="currency">$200,000,000</td> + </tr> + <tr> + <th scope="row">Shrek Forever After</th> + <td data-title="Released">2010</td> + <td data-title="Studio">Dreamworks</td> + <td data-title="Worldwide Gross" data-type="currency">$752,600,867</td> + <td data-title="Domestic Gross" data-type="currency">$238,736,787</td> + <td data-title="Foreign Gross" data-type="currency">$513,864,080</td> + <td data-title="Budget" data-type="currency">$165,000,000</td> + </tr> + </tbody> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_tree.xhtml b/accessible/tests/mochitest/tree/test_tree.xhtml new file mode 100644 index 0000000000..890a46209c --- /dev/null +++ b/accessible/tests/mochitest/tree/test_tree.xhtml @@ -0,0 +1,180 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tree hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Accessible tree testers + + function getTreeItemAccTree(aTableRole, acolumnCount) + { + var treeItemRole; + switch (aTableRole) { + case ROLE_LIST: + treeItemRole = ROLE_LISTITEM; + break; + case ROLE_OUTLINE: + treeItemRole = ROLE_OUTLINEITEM; + break; + case ROLE_TABLE: case ROLE_TREE_TABLE: + treeItemRole = ROLE_ROW; + break; + } + + var accTree = { + role: treeItemRole, + children: [] + }; + + if (aTableRole == ROLE_TABLE || aTableRole == ROLE_TREE_TABLE) { + for (var idx = 0; idx < acolumnCount; idx++) { + var cellAccTree = { + role: ROLE_GRID_CELL, + children: [] + }; + accTree.children.push(cellAccTree); + } + } + + return accTree; + } + + function testAccessibleTreeFor(aTree, aRole) + { + var accTreeForColumns = { + role: ROLE_LIST, + children: [] + }; + + var accTreeForTree = { + role: aRole, + children: [ + accTreeForColumns + ] + }; + + var view = aTree.view; + var columnCount = aTree.columns.count; + + for (let idx = 0; idx < columnCount; idx++) + accTreeForColumns.children.push({ COLUMNHEADER: [ ] }); + if (!aTree.hasAttribute("hidecolumnpicker")) + accTreeForColumns.children.push({ PUSHBUTTON: [ { MENUPOPUP: [] } ] }); + + for (let idx = 0; idx < view.rowCount; idx++) + accTreeForTree.children.push(getTreeItemAccTree(aRole, columnCount)); + + testAccessibleTree(aTree, accTreeForTree); + } + + /** + * Event queue invoker object to test accessible tree for XUL tree element. + */ + function treeChecker(aID, aView, aRole) + { + this.DOMNode = getNode(aID); + + this.invoke = function invoke() + { + this.DOMNode.view = aView; + } + this.check = function check(aEvent) + { + testAccessibleTreeFor(this.DOMNode, aRole); + } + this.getID = function getID() + { + return "Tree testing of " + aID; + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; + var gQueue = null; + + function doTest() + { + gQueue = new eventQueue(EVENT_REORDER); + + gQueue.push(new treeChecker("list", new nsTableTreeView(3), ROLE_LIST)); + gQueue.push(new treeChecker("tree", new nsTreeTreeView(), ROLE_OUTLINE)); + gQueue.push(new treeChecker("table", new nsTableTreeView(3), ROLE_TABLE)); + gQueue.push(new treeChecker("treetable", new nsTreeTreeView(), ROLE_TREE_TABLE)); + + gQueue.invoke(); // Will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Mozilla Bug 503727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tree id="list" flex="1" hidecolumnpicker="true"> + <treecols> + <treecol id="col" flex="1" hideheader="true"/> + </treecols> + <treechildren/> + </tree> + + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <tree id="table" flex="1"> + <treecols> + <treecol id="col1" flex="1" label="column"/> + <treecol id="col2" flex="1" label="column 2"/> + </treecols> + <treechildren/> + </tree> + + <tree id="treetable" flex="1"> + <treecols> + <treecol id="col1" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="column 2"/> + </treecols> + <treechildren/> + </tree> + + <vbox id="debug"/> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/tree/test_txtcntr.html b/accessible/tests/mochitest/tree/test_txtcntr.html new file mode 100644 index 0000000000..4c44701a41 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_txtcntr.html @@ -0,0 +1,234 @@ +<!DOCTYPE html> +<html> + +<head> + <title>HTML text containers tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + var accTree = { + role: ROLE_SECTION, + children: [ + { // text child + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + + testAccessibleTree("c1", accTree); + testAccessibleTree("c2", accTree); + + accTree = { + role: ROLE_SECTION, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Hello1", + }, + { + role: ROLE_WHITESPACE, + }, + { + role: ROLE_TEXT_LEAF, + name: "Hello2", + }, + { + role: ROLE_SEPARATOR, + }, + { + role: ROLE_TEXT_LEAF, + name: "Hello3 ", + }, + { + role: ROLE_PARAGRAPH, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "Hello4 ", + }, + ], + }, + ], + }; + + testAccessibleTree("c3", accTree); + + // contentEditable div + accTree = { + role: ROLE_SECTION, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "helllo ", + }, + { + role: ROLE_PARAGRAPH, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "blabla", + }, + ], + }, + { + role: ROLE_TEXT_LEAF, + name: "hello ", + }, + ], + }; + + testAccessibleTree("c4", accTree); + + // blockquote + accTree = { + role: ROLE_SECTION, + children: [ + { // block quote + role: ROLE_BLOCKQUOTE, + children: [ + { // text child + role: ROLE_TEXT_LEAF, + name: "Hello", + children: [], + }, + ], + }, + ], + }; + + testAccessibleTree("c5", accTree); + + // abbreviation tag + accTree = { + role: ROLE_SECTION, + children: [ + { // text leaf + role: ROLE_TEXT_LEAF, + name: "This ", + children: [], + }, + { // abbr tag + role: ROLE_TEXT, + name: "accessibility", + children: [ + { // text leaf with actual text + role: ROLE_TEXT_LEAF, + name: "a11y", + children: [], + }, + ], + }, + { // text leaf + role: ROLE_TEXT_LEAF, + name: " test", + children: [], + }, + ], + }; + + testAccessibleTree("c6", accTree); + + // acronym tag + accTree = { + role: ROLE_SECTION, + children: [ + { // text leaf + role: ROLE_TEXT_LEAF, + name: "This ", + children: [], + }, + { // acronym tag + role: ROLE_TEXT, + name: "personal computer", + children: [ + { // text leaf with actual text + role: ROLE_TEXT_LEAF, + name: "PC", + children: [], + }, + ], + }, + { // text leaf + role: ROLE_TEXT_LEAF, + name: " is broken", + children: [], + }, + ], + }; + + testAccessibleTree("c7", accTree); + + // only whitespace between images should be exposed + accTree = { + SECTION: [ + { GRAPHIC: [] }, + { TEXT_LEAF: [] }, + { GRAPHIC: [] }, + ], + }; + testAccessibleTree("c8", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="overflowed content doesn't expose child text accessibles" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=489306"> + Mozilla Bug 489306</a> + <a target="_blank" + title="Create child accessibles for text controls from native anonymous content" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=542824"> + Mozilla Bug 542824</a> + <a target="_blank" + title="Update accessible tree on content insertion after layout" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=498015"> + Mozilla Bug 498015</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="c1" style="width: 100px; height: 100px; overflow: auto;"> + 1hellohello 2hellohello 3hellohello 4hellohello 5hellohello 6hellohello 7hellohello + </div> + <div id="c2"> + 1hellohello 2hellohello 3hellohello 4hellohello 5hellohello 6hellohello 7hellohello + </div> + <div id="c3"> + Hello1<br> + Hello2<hr> + Hello3 + <p> + Hello4 + </p> + </div> + <div id="c4" contentEditable="true"> + helllo <p>blabla</p> hello + </div> + <div id="c5"><blockquote>Hello</blockquote></div> + <div id="c6">This <abbr title="accessibility">a11y</abbr> test</div> + <div id="c7">This <acronym title="personal computer">PC</acronym> is broken</div> + + <!-- Whitespace between images should be exposed. Whitespace between the + div and img tags will be inconsistent depending on the image cache + state and what optimizations layout was able to apply. --> + <div id="c8"><img src="../moz.png"> <img src="../moz.png"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_txtctrl.html b/accessible/tests/mochitest/tree/test_txtctrl.html new file mode 100644 index 0000000000..ee66fbf801 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_txtctrl.html @@ -0,0 +1,171 @@ +<!DOCTYPE html> +<html> + +<head> + <title>HTML text controls tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script type="application/javascript"> + function doTest() { + // editable div + var accTree = { + role: ROLE_SECTION, + children: [ + { // text child + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + + testAccessibleTree("txc1", accTree); + + // input@type="text", value + accTree = { + role: ROLE_ENTRY, + children: [ + { // text child + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + + testAccessibleTree("txc2", accTree); + + // input@type="text", no value + accTree = + { ENTRY: [ ] }; + + testAccessibleTree("txc3", accTree); + + // textarea + accTree = { + role: ROLE_ENTRY, + children: [ + { + role: ROLE_TEXT_LEAF, // hello1\nhello2 text + }, + ], + }; + + testAccessibleTree("txc4", accTree); + + // input@type="password" + accTree = { + role: ROLE_PASSWORD_TEXT, + children: [ + { + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + + testAccessibleTree("txc5", accTree); + + // input@type="tel", value + accTree = { + role: ROLE_ENTRY, + children: [ + { // text child + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + + testAccessibleTree("txc6", accTree); + + // input@type="email", value + accTree = { + role: ROLE_EDITCOMBOBOX, // Because of list attribute + children: [ + { // text child + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + + testAccessibleTree("txc7", accTree); + + // input@type="search", value + accTree = { + role: ROLE_EDITCOMBOBOX, // Because of list attribute + children: [ + { // text child + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + + testAccessibleTree("txc8", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="overflowed content doesn't expose child text accessibles" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=489306"> + Mozilla Bug 489306 + </a><br> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=542824" + title="Create child accessibles for text controls from native anonymous content"> + Mozilla Bug 542824 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=625652" + title="Make sure accessible tree is correct when rendered text is changed"> + Mozilla Bug 625652 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="txc1" contentEditable="true"> + 1hellohello + </div> + <input id="txc2" value="hello"> + <input id="txc3"> + <textarea id="txc4"> + hello1 + hello2 + </textarea> + <input id="txc5" type="password" value="hello"> + <input id="txc6" type="tel" value="4167771234"> + + Email Address: + <input id="txc7" type="email" list="contacts" value="xyzzy"> + <datalist id="contacts"> + <option>xyzzy@plughs.com</option> + <option>nobody@mozilla.org</option> + </datalist> + + </br>Search for: + <input id="txc8" type="search" list="searchhisty" value="Gamma"> + <datalist id="searchhisty"> + <option>Gamma Rays</option> + <option>Gamma Ray Bursts</option> + </datalist> + +</body> +</html> diff --git a/accessible/tests/mochitest/tree/test_txtctrl.xhtml b/accessible/tests/mochitest/tree/test_txtctrl.xhtml new file mode 100644 index 0000000000..ff3d4977f0 --- /dev/null +++ b/accessible/tests/mochitest/tree/test_txtctrl.xhtml @@ -0,0 +1,86 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL textbox and textarea hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //gA11yEventDumpToConsole = true; + //enableLogging("tree,verbose"); // debug stuff + + //////////////////////////////////////////////////////////////////////////// + // Test + + function doTest() + { + ////////////////////////////////////////////////////////////////////////// + // search textbox + let accTree = + { GROUPING: [ + { ENTRY: [ { TEXT_LEAF: [] } ] }, + ] }; + testAccessibleTree("txc_search", accTree); + + ////////////////////////////////////////////////////////////////////////// + // search textbox with search button + + if (MAC) { + accTree = + { GROUPING: [ + { ENTRY: [ { TEXT_LEAF: [] } ] }, + ] }; + } else { + accTree = + { GROUPING: [ + { ENTRY: [ { TEXT_LEAF: [] } ] }, + { PUSHBUTTON: [] }, + ] }; + } + + testAccessibleTree("txc_search_searchbutton", accTree); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=542824" + title="Create child accessibles for text controls from native anonymous content"> + Mozilla Bug 542824 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <box role="group" id="txc_search"> + <search-textbox value="hello"/> + </box> + <box role="group" id="txc_search_searchbutton"> + <search-textbox searchbutton="true" value="hello"/> + </box> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/tree/wnd.xhtml b/accessible/tests/mochitest/tree/wnd.xhtml new file mode 100644 index 0000000000..3b87cb5e0d --- /dev/null +++ b/accessible/tests/mochitest/tree/wnd.xhtml @@ -0,0 +1,8 @@ +<?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" + title="Empty Window"> + +</window> + diff --git a/accessible/tests/mochitest/treeupdate/a11y.ini b/accessible/tests/mochitest/treeupdate/a11y.ini new file mode 100644 index 0000000000..b52eef3c84 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/a11y.ini @@ -0,0 +1,44 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + !/accessible/tests/mochitest/letters.gif + !/accessible/tests/mochitest/moz.png + +[test_ariadialog.html] +[test_ariahidden.html] +[test_ariaowns.html] +[test_bug852150.xhtml] +[test_bug883708.xhtml] +[test_bug884251.xhtml] +[test_bug895082.html] +[test_bug1040735.html] +[test_bug1100602.html] +[test_bug1175913.html] +[test_bug1189277.html] +[test_bug1276857.html] +support-files = test_bug1276857_subframe.html +[test_canvas.html] +[test_contextmenu.xhtml] +[test_cssoverflow.html] +[test_deck.xhtml] +[test_delayed_removal.html] +[test_doc.html] +[test_gencontent.html] +[test_general.html] +[test_hidden.html] +[test_imagemap.html] +[test_list.html] +[test_list_editabledoc.html] +[test_listbox.xhtml] +[test_menu.xhtml] +[test_menubutton.xhtml] +[test_optgroup.html] +[test_recreation.html] +[test_select.html] +[test_shadow_slots.html] +[test_shutdown.xhtml] +[test_table.html] +[test_textleaf.html] +[test_tooltip.xhtml] +[test_visibility.html] +[test_whitespace.html] diff --git a/accessible/tests/mochitest/treeupdate/test_ariadialog.html b/accessible/tests/mochitest/treeupdate/test_ariadialog.html new file mode 100644 index 0000000000..d7e6fa58fb --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_ariadialog.html @@ -0,0 +1,113 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Table creation in ARIA dialog test</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function showARIADialog(aID) { + this.node = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.node), + ]; + + this.invoke = function showARIADialog_invoke() { + getNode("dialog").style.display = "block"; + getNode("table").style.visibility = "visible"; + getNode("a").textContent = "link"; + getNode("input").value = "hello"; + getNode("input").focus(); + }; + + this.finalCheck = function showARIADialog_finalCheck() { + var tree = { + role: ROLE_DIALOG, + children: [ + { + role: ROLE_PUSHBUTTON, + children: [ { role: ROLE_TEXT_LEAF } ], + }, + { + role: ROLE_ENTRY, + }, + ], + }; + testAccessibleTree(aID, tree); + }; + + this.getID = function showARIADialog_getID() { + return "show ARIA dialog"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTest() { + // enableLogging("tree"); + gQueue = new eventQueue(); + + // make the accessible an inaccessible + gQueue.push(new showARIADialog("dialog")); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Rework accessible tree update code" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"> + Mozilla Bug 570275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <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; visibility: hidden;"> + <tbody> + <tr> + <td role="presentation"> + <div role="presentation"> + <a id="a" role="button">text</a> + </div> + <input id="input"> + </td> + </tr> + </tbody> + </table> + </div> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_ariahidden.html b/accessible/tests/mochitest/treeupdate/test_ariahidden.html new file mode 100644 index 0000000000..302465b59f --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_ariahidden.html @@ -0,0 +1,118 @@ +<html> + +<head> + <title>aria-hidden tree update tests</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function t1_setARIAHidden() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, "t1"), + ]; + + this.invoke = function t1_setARIAHidden_invoke() { + getNode("t1_child").setAttribute("aria-hidden", "true"); + }; + + this.finalCheck = function t1_setARIAHidden_finalCheck() { + ok(!isAccessible("t1_child"), "No accessible for aria-hidden"); + }; + + this.getID = function t1_setARIAHidden_getID() { + return "aria-hidden set to true"; + }; + } + + function t1_removeARIAHidden() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, "t1"), + ]; + + this.invoke = function t1_removeARIAHidden_invoke() { + getNode("t1_child").removeAttribute("aria-hidden"); + }; + + this.finalCheck = function t1_removeARIAHidden_finalCheck() { + ok(isAccessible("t1_child"), "No aria-hidden, has to be accessible"); + }; + + this.getID = function t1_removeARIAHidden_getID() { + return "remove aria-hidden"; + }; + } + + function t2_setARIAHidden() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, "t2"), + ]; + + this.invoke = function t2_setARIAHidden_invoke() { + getNode("t2_child").setAttribute("aria-hidden", "true"); + }; + + this.finalCheck = function t2_setARIAHidden_finalCheck() { + testAccessibleTree("t2", { SECTION: []}); + }; + + this.getID = function t2_setARIAHidden_getID() { + return "t2: set aria-hidden"; + }; + } + + function t2_insertUnderARIAHidden() { + this.eventSeq = [ + new unexpectedInvokerChecker(EVENT_REORDER, "t2"), + ]; + + this.invoke = function t2_insertUnderARIAHidden_invoke() { + getNode("t2_child").innerHTML = "<input>"; + }; + + this.finalCheck = function t2_insertUnderARIAHidden_finalCheck() { + testAccessibleTree("t2", { SECTION: []}); + }; + + this.getID = function t2_insertUnderARIAHidden_getID() { + return "t2: insert under aria-hidden"; + }; + } + + // gA11yEventDumpToConsole = true; + function doTests() { + ok(!isAccessible("t1_child"), "No accessible for aria-hidden"); + + const gQueue = new eventQueue(); + gQueue.push(new t1_removeARIAHidden()); + gQueue.push(new t1_setARIAHidden()); + gQueue.push(new t2_setARIAHidden()); + gQueue.push(new t2_insertUnderARIAHidden()); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="t1"><div id="t1_child" aria-hidden="true">Hi</div><div>there</div></div> + <div id="t2"> + <span id="t2_child">hoho</span> + </div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_ariaowns.html b/accessible/tests/mochitest/treeupdate/test_ariaowns.html new file mode 100644 index 0000000000..200a302085 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_ariaowns.html @@ -0,0 +1,797 @@ +<!DOCTYPE html> +<html> + +<head> + <title>@aria-owns attribute testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + // ////////////////////////////////////////////////////////////////////////// + + function changeARIAOwns() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t1_button")), + // no hide for t1_subdiv because it is contained by hidden t1_checkbox + new invokerChecker(EVENT_HIDE, getNode("t1_checkbox")), + new invokerChecker(EVENT_SHOW, getNode("t1_checkbox")), + new invokerChecker(EVENT_SHOW, getNode("t1_button")), + new invokerChecker(EVENT_SHOW, getNode("t1_subdiv")), + new invokerChecker(EVENT_REORDER, getNode("t1_container")), + ]; + + this.invoke = function setARIAOwns_invoke() { + // children are swapped by ARIA owns + var tree = + { SECTION: [ + { CHECKBUTTON: [ + { SECTION: [] }, + ] }, + { PUSHBUTTON: [ ] }, + ] }; + testAccessibleTree("t1_container", tree); + + getNode("t1_container"). + setAttribute("aria-owns", "t1_button t1_subdiv"); + }; + + this.finalCheck = function setARIAOwns_finalCheck() { + // children are swapped again, button and subdiv are appended to + // the children. + var tree = + { SECTION: [ + { CHECKBUTTON: [ ] }, // checkbox, native order + { PUSHBUTTON: [ ] }, // button, rearranged by ARIA own + { SECTION: [ ] }, // subdiv from the subtree, ARIA owned + ] }; + testAccessibleTree("t1_container", tree); + }; + + this.getID = function setARIAOwns_getID() { + return "Change @aria-owns attribute"; + }; + } + + function removeARIAOwns() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t1_button")), + new invokerChecker(EVENT_HIDE, getNode("t1_subdiv")), + new orderChecker(), + new asyncInvokerChecker(EVENT_SHOW, getNode("t1_button")), + new asyncInvokerChecker(EVENT_SHOW, getNode("t1_subdiv")), + new orderChecker(), + new invokerChecker(EVENT_REORDER, getNode("t1_container")), + new unexpectedInvokerChecker(EVENT_REORDER, getNode("t1_checkbox")), + ]; + + this.invoke = function removeARIAOwns_invoke() { + getNode("t1_container").removeAttribute("aria-owns"); + }; + + this.finalCheck = function removeARIAOwns_finalCheck() { + // children follow the DOM order + var tree = + { SECTION: [ + { PUSHBUTTON: [ ] }, + { CHECKBUTTON: [ + { SECTION: [] }, + ] }, + ] }; + testAccessibleTree("t1_container", tree); + }; + + this.getID = function removeARIAOwns_getID() { + return "Remove @aria-owns attribute"; + }; + } + + function setARIAOwns() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t1_button")), + new invokerChecker(EVENT_HIDE, getNode("t1_subdiv")), + new invokerChecker(EVENT_SHOW, getNode("t1_button")), + new invokerChecker(EVENT_SHOW, getNode("t1_subdiv")), + new invokerChecker(EVENT_REORDER, getNode("t1_container")), + ]; + + this.invoke = function setARIAOwns_invoke() { + getNode("t1_container"). + setAttribute("aria-owns", "t1_button t1_subdiv"); + }; + + this.finalCheck = function setARIAOwns_finalCheck() { + // children are swapped again, button and subdiv are appended to + // the children. + var tree = + { SECTION: [ + { CHECKBUTTON: [ ] }, // checkbox + { PUSHBUTTON: [ ] }, // button, rearranged by ARIA own + { SECTION: [ ] }, // subdiv from the subtree, ARIA owned + ] }; + testAccessibleTree("t1_container", tree); + }; + + this.getID = function setARIAOwns_getID() { + return "Set @aria-owns attribute"; + }; + } + + function addIdToARIAOwns() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t1_group")), + new invokerChecker(EVENT_SHOW, getNode("t1_group")), + new invokerChecker(EVENT_REORDER, document), + ]; + + this.invoke = function addIdToARIAOwns_invoke() { + getNode("t1_container"). + setAttribute("aria-owns", "t1_button t1_subdiv t1_group"); + }; + + this.finalCheck = function addIdToARIAOwns_finalCheck() { + // children are swapped again, button and subdiv are appended to + // the children. + var tree = + { SECTION: [ + { CHECKBUTTON: [ ] }, // t1_checkbox + { PUSHBUTTON: [ ] }, // button, t1_button + { SECTION: [ ] }, // subdiv from the subtree, t1_subdiv + { GROUPING: [ ] }, // group from outside, t1_group + ] }; + testAccessibleTree("t1_container", tree); + }; + + this.getID = function addIdToARIAOwns_getID() { + return "Add id to @aria-owns attribute value"; + }; + } + + function appendEl() { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getNode, "t1_child3"), + new invokerChecker(EVENT_REORDER, getNode("t1_container")), + ]; + + this.invoke = function appendEl_invoke() { + var div = document.createElement("div"); + div.setAttribute("id", "t1_child3"); + div.setAttribute("role", "radio"); + getNode("t1_container").appendChild(div); + }; + + this.finalCheck = function appendEl_finalCheck() { + // children are invalidated, they includes aria-owns swapped kids and + // newly inserted child. + var 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("t1_container", tree); + }; + + this.getID = function appendEl_getID() { + return "Append child under @aria-owns element"; + }; + } + + function removeEl() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t1_subdiv")), + new invokerChecker(EVENT_REORDER, getNode("t1_container")), + ]; + + this.invoke = function removeEl_invoke() { + // remove a container of t1_subdiv + getNode("t1_span").remove(); + }; + + this.finalCheck = function removeEl_finalCheck() { + // subdiv should go away + var tree = + { SECTION: [ + { CHECKBUTTON: [ ] }, // explicit, t1_checkbox + { RADIOBUTTON: [ ] }, // explicit, t1_child3 + { PUSHBUTTON: [ ] }, // ARIA owned, t1_button + { GROUPING: [ ] }, // ARIA owned, t1_group + ] }; + testAccessibleTree("t1_container", tree); + }; + + this.getID = function removeEl_getID() { + return "Remove a container of ARIA owned element"; + }; + } + + function removeId() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t1_group")), + new invokerChecker(EVENT_SHOW, getNode("t1_group")), + new invokerChecker(EVENT_REORDER, document), + ]; + + this.invoke = function removeId_invoke() { + getNode("t1_group").removeAttribute("id"); + }; + + this.finalCheck = function removeId_finalCheck() { + var tree = + { SECTION: [ + { CHECKBUTTON: [ ] }, + { RADIOBUTTON: [ ] }, + { PUSHBUTTON: [ ] }, // ARIA owned, t1_button + ] }; + testAccessibleTree("t1_container", tree); + }; + + this.getID = function removeId_getID() { + return "Remove ID from ARIA owned element"; + }; + } + + function setId() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("t1_grouptmp")), + new invokerChecker(EVENT_SHOW, getNode("t1_grouptmp")), + new invokerChecker(EVENT_REORDER, document), + ]; + + this.invoke = function setId_invoke() { + getNode("t1_grouptmp").setAttribute("id", "t1_group"); + }; + + this.finalCheck = function setId_finalCheck() { + var tree = + { SECTION: [ + { CHECKBUTTON: [ ] }, + { RADIOBUTTON: [ ] }, + { PUSHBUTTON: [ ] }, // ARIA owned, t1_button + { GROUPING: [ ] }, // ARIA owned, t1_group, previously t1_grouptmp + ] }; + testAccessibleTree("t1_container", tree); + }; + + this.getID = function setId_getID() { + return "Set ID that is referred by ARIA owns"; + }; + } + + /** + * Remove an accessible DOM element containing an element referred by + * ARIA owns. + */ + function removeA11eteiner() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getNode("t2_container1")), + ]; + + this.invoke = function removeA11eteiner_invoke() { + var tree = + { SECTION: [ + { CHECKBUTTON: [ ] }, // ARIA owned, 't2_owned' + ] }; + testAccessibleTree("t2_container1", tree); + + getNode("t2_container2").removeChild(getNode("t2_container3")); + }; + + this.finalCheck = function removeA11eteiner_finalCheck() { + var tree = + { SECTION: [ + ] }; + testAccessibleTree("t2_container1", tree); + }; + + this.getID = function removeA11eteiner_getID() { + return "Remove an accessible DOM element containing an element referred by ARIA owns"; + }; + } + + /** + * Attempt to steal an element from other ARIA owns element. This should + * not be possible. The only child that will get owned into this + * container is a previously not aria-owned one. + */ + function stealFromOtherARIAOwns() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getNode("t3_container3")), + ]; + + this.invoke = function stealFromOtherARIAOwns_invoke() { + getNode("t3_container3").setAttribute("aria-owns", "t3_child t3_child2"); + }; + + this.finalCheck = function stealFromOtherARIAOwns_finalCheck() { + var tree = + { SECTION: [ + { CHECKBUTTON: [ + ] }, + ] }; + testAccessibleTree("t3_container1", tree); + + tree = + { SECTION: [ + ] }; + testAccessibleTree("t3_container2", tree); + + tree = + { SECTION: [ + { CHECKBUTTON: [ + ] }, + ] }; + testAccessibleTree("t3_container3", tree); + }; + + this.getID = function stealFromOtherARIAOwns_getID() { + return "Steal an element from other ARIA owns element"; + }; + } + + function appendElToRecacheChildren() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getNode("t3_container3")), + ]; + + this.invoke = function appendElToRecacheChildren_invoke() { + var div = document.createElement("div"); + div.setAttribute("role", "radio"); + getNode("t3_container3").appendChild(div); + }; + + this.finalCheck = function appendElToRecacheChildren_finalCheck() { + var tree = + { SECTION: [ + ] }; + testAccessibleTree("t3_container2", tree); + + tree = + { SECTION: [ + { RADIOBUTTON: [ ] }, + { CHECKBUTTON: [ ] }, // ARIA owned + ] }; + testAccessibleTree("t3_container3", tree); + }; + + this.getID = function appendElToRecacheChildren_getID() { + return "Append a child under @aria-owns element to trigger children recache"; + }; + } + + function showHiddenElement() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getNode("t4_container1")), + ]; + + this.invoke = function showHiddenElement_invoke() { + var tree = + { SECTION: [ + { RADIOBUTTON: [] }, + ] }; + testAccessibleTree("t4_container1", tree); + + getNode("t4_child1").style.display = "block"; + }; + + this.finalCheck = function showHiddenElement_finalCheck() { + var tree = + { SECTION: [ + { CHECKBUTTON: [] }, + { RADIOBUTTON: [] }, + ] }; + testAccessibleTree("t4_container1", tree); + }; + + this.getID = function showHiddenElement_getID() { + return "Show hidden ARIA owns referred element"; + }; + } + + function rearrangeARIAOwns(aContainer, aAttr, aIdList, aRoleList) { + this.eventSeq = []; + for (let id of aIdList) { + this.eventSeq.push(new invokerChecker(EVENT_HIDE, getNode(id))); + } + + for (let id of aIdList) { + this.eventSeq.push(new invokerChecker(EVENT_SHOW, getNode(id))); + } + this.eventSeq.push(new invokerChecker(EVENT_REORDER, getNode(aContainer))); + + this.invoke = function rearrangeARIAOwns_invoke() { + getNode(aContainer).setAttribute("aria-owns", aAttr); + }; + + this.finalCheck = function rearrangeARIAOwns_finalCheck() { + var tree = { SECTION: [ ] }; + for (var role of aRoleList) { + var ch = {}; + ch[role] = []; + tree.SECTION.push(ch); + } + testAccessibleTree(aContainer, tree); + }; + + this.getID = function rearrangeARIAOwns_getID() { + return `Rearrange @aria-owns attribute to '${aAttr}'`; + }; + } + + function removeNotARIAOwnedEl(aContainer, aChild) { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, aContainer), + ]; + + this.invoke = function removeNotARIAOwnedEl_invoke() { + var tree = { + SECTION: [ + { TEXT_LEAF: [ ] }, + { GROUPING: [ ] }, + ], + }; + testAccessibleTree(aContainer, tree); + + getNode(aContainer).removeChild(getNode(aChild)); + }; + + this.finalCheck = function removeNotARIAOwnedEl_finalCheck() { + var tree = { + SECTION: [ + { GROUPING: [ ] }, + ], + }; + testAccessibleTree(aContainer, tree); + }; + + this.getID = function removeNotARIAOwnedEl_getID() { + return `remove not ARIA owned child`; + }; + } + + function setARIAOwnsOnElToRemove(aParent, aChild) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getAccessible(aParent)), + ]; + + this.invoke = function setARIAOwnsOnElToRemove_invoke() { + getNode(aChild).setAttribute("aria-owns", "no_id"); + getNode(aParent).removeChild(getNode(aChild)); + getNode(aParent).remove(); + }; + + this.getID = function setARIAOwnsOnElToRemove_getID() { + return `set ARIA owns on an element, and then remove it, and then remove its parent`; + }; + } + + /** + * Set ARIA owns on inaccessible span element that contains + * accessible children. This will move children from the container for + * the span. + */ + function test8() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, "t8_container"), + ]; + + this.invoke = function test8_invoke() { + var tree = + { SECTION: [ + { PUSHBUTTON: [] }, + { ENTRY: [] }, + { ENTRY: [] }, + { ENTRY: [] }, + ] }; + testAccessibleTree("t8_container", tree); + + getNode(t8_container).setAttribute("aria-owns", "t8_span t8_button"); + }; + + this.finalCheck = function test8_finalCheck() { + var tree = + { SECTION: [ + { TEXT: [ + { ENTRY: [] }, + { ENTRY: [] }, + { ENTRY: [] }, + ] }, + { PUSHBUTTON: [] }, + ] }; + testAccessibleTree("t8_container", tree); + }; + + this.getID = function test8_getID() { + return `Set ARIA owns on inaccessible span element that contains accessible children`; + }; + } + + function test9_prepare() { + this.eventSeq = [ + new invokerChecker(EVENT_DOCUMENT_LOAD_COMPLETE, () => { + return getNode("t9_container").contentDocument; + }), + ]; + + this.invoke = () => { + // The \ before the final /script avoids the script from being terminated + // by the html parser. + getNode("t9_container").src = `data:text/html, + <html><body></body> + <script> + let el = document.createElement('div'); + el.id = 'container'; + el.innerHTML = "<input id='input'>"; + document.documentElement.appendChild(el); + <\/script></html>`; + }; + + this.finalCheck = () => { + var tree = + { INTERNAL_FRAME: [ + { DOCUMENT: [ + { SECTION: [ + { ENTRY: [] }, + ] }, + ] }, + ] }; + testAccessibleTree("t9_container", tree); + }; + + this.getID = () => { + return `Set ARIA owns on a document (part1)`; + }; + } + + function test9_setARIAOwns() { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, () => { + let doc = getNode("t9_container").contentDocument; + return doc && doc.getElementById("input"); + }), + ]; + + this.invoke = () => { + let doc = getNode("t9_container").contentDocument; + doc.body.setAttribute("aria-owns", "input"); + }; + + this.finalCheck = () => { + var tree = + { INTERNAL_FRAME: [ + { DOCUMENT: [ + { SECTION: [] }, + { ENTRY: [] }, + ] }, + ] }; + testAccessibleTree("t9_container", tree); + }; + + this.getID = () => { + return `Set ARIA owns on a document (part2)`; + }; + } + + function test9_finish() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, () => { + return getNode("t9_container").contentDocument; + }), + ]; + + this.invoke = () => { + // trigger a tree update. + let doc = getNode("t9_container").contentDocument; + doc.body.appendChild(doc.createElement("p")); + }; + + this.finalCheck = () => { + var tree = + { INTERNAL_FRAME: [ + { DOCUMENT: [ + { PARAGRAPH: [] }, + { SECTION: [] }, + { ENTRY: [] }, + ] }, + ] }; + testAccessibleTree("t9_container", tree); + }; + + this.getID = () => { + return `Set ARIA owns on a document (part3)`; + }; + } + + /** + * Put ARIA owned child back when ARIA owner removed. + */ + function test10_removeARIAOwner() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getAccessible("t10_owner")), + ]; + + this.invoke = () => { + let tree = + { SECTION: [ // t10_container + { SECTION: [ // t10_owner + { ENTRY: [] }, // t10_child + ] }, + ] }; + testAccessibleTree("t10_container", tree); + + getNode("t10_owner").remove(); + }; + + this.getID = () => { + return "Put aria owned child back when aria owner removed"; + }; + } + + function test10_finishTest() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, "t10_container"), + ]; + + this.invoke = () => { + // trigger a tree update. + getNode("t10_container").append(document.createElement("p")); + }; + + this.finalCheck = () => { + let tree = + { SECTION: [ // t10_container + // { ENTRY: [] }, // t10_child + { PARAGRAPH: [] }, + ] }; + testAccessibleTree("t10_container", tree); + todo(false, "Input accessible has be moved back in the tree"); + }; + + this.getID = () => { + return `Put aria owned child back when aria owner removed (finish test)`; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + // ////////////////////////////////////////////////////////////////////////// + + // gA11yEventDumpToConsole = true; + // enableLogging("tree,eventTree,verbose"); // debug stuff + + var gQueue = null; + + function doTest() { + gQueue = new eventQueue(); + + // test1 + gQueue.push(new changeARIAOwns()); + gQueue.push(new removeARIAOwns()); + gQueue.push(new setARIAOwns()); + gQueue.push(new addIdToARIAOwns()); + gQueue.push(new appendEl()); + gQueue.push(new removeEl()); + gQueue.push(new removeId()); + gQueue.push(new setId()); + + // test2 + gQueue.push(new removeA11eteiner()); + + // test3 + gQueue.push(new stealFromOtherARIAOwns()); + gQueue.push(new appendElToRecacheChildren()); + + // test4 + gQueue.push(new showHiddenElement()); + + // test5 + gQueue.push(new rearrangeARIAOwns( + "t5_container", "t5_checkbox t5_radio t5_button", + [ "t5_checkbox", "t5_radio", "t5_button" ], + [ "CHECKBUTTON", "RADIOBUTTON", "PUSHBUTTON" ])); + gQueue.push(new rearrangeARIAOwns( + "t5_container", "t5_radio t5_button t5_checkbox", + [ "t5_radio", "t5_button" ], + [ "RADIOBUTTON", "PUSHBUTTON", "CHECKBUTTON" ])); + + gQueue.push(new removeNotARIAOwnedEl("t6_container", "t6_span")); + + gQueue.push(new setARIAOwnsOnElToRemove("t7_parent", "t7_child")); + + gQueue.push(new test8()); + gQueue.push(new test9_prepare()); + gQueue.push(new test9_setARIAOwns()); + gQueue.push(new test9_finish()); + + gQueue.push(new test10_removeARIAOwner()); + gQueue.push(new test10_finishTest()); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + + </script> +</head> + +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <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 id="t3_child2" role="checkbox"></div> + </div> + <div id="t3_container3"></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> + + <div id="t7_container"> + <div id="t7_parent"> + <div id="t7_child"></div> + </div> + </div> + + <div id="t8_container"> + <input id="t8_button" type="button"><span id="t8_span"><input><input><input></span> + </div> + + <iframe id="t9_container"></iframe> + + <div id="t10_container"> + <div id="t10_owner" aria-owns="t10_child"></div> + <input id="t10_child"> + </div> +</body> + +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug1040735.html b/accessible/tests/mochitest/treeupdate/test_bug1040735.html new file mode 100644 index 0000000000..b7d0e472d0 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug1040735.html @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> +<head> + <title>Adopt DOM node from anonymous subtree</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script type="application/javascript" + src="../common.js"></script> + + <script type="application/javascript"> + function doTest() { + document.body.appendChild(document.getElementById("mw_a")); + setTimeout(function() { ok(true, "no crash and assertions"); SimpleTest.finish(); }, 0); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> + +<body> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1040735" + title="Bug 1040735 - DOM node reinsertion under anonymous content may trigger a11y child adoption"> + Bug 1040735</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <marquee> + <div id="mw_a" style="visibility: hidden;"> + <div style="visibility: visible;" id="mw_inside"></div> + </div> + </marquee> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug1100602.html b/accessible/tests/mochitest/treeupdate/test_bug1100602.html new file mode 100644 index 0000000000..7233b585c1 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug1100602.html @@ -0,0 +1,103 @@ +<html> + +<head> + <title>Test hide/show events for HTMLListBulletAccessibles on list restyle</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../name.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + /** + * Change list style type to none. + */ + function hideBullet() { + this.eventSeq = []; + this.liAcc = getAccessible("list_element"); + this.bullet = this.liAcc.firstChild; + + this.eventSeq.push(new invokerChecker(EVENT_HIDE, this.bullet)); + this.eventSeq.push(new invokerChecker(EVENT_REORDER, this.liAcc)); + + this.invoke = function hideBullet_invoke() { + getNode("list").setAttribute("style", "list-style-type: none;"); + }; + + this.finalCheck = function hideBullet_finalCheck() { + is(this.liAcc.name, "list element", + "Check that first child of LI is not a bullet."); + }; + + this.getID = function hideBullet_getID() { + return "Hide bullet by setting style to none"; + }; + } + + /** + * Change list style type to circles. + */ + function showBullet() { + this.eventSeq = []; + this.liAcc = getAccessible("list_element"); + + this.eventSeq.push(new invokerChecker(EVENT_SHOW, + function(aNode) { return aNode.firstChild; }, + this.liAcc)); + this.eventSeq.push(new invokerChecker(EVENT_REORDER, this.liAcc)); + + this.invoke = function showBullet_invoke() { + getNode("list").setAttribute("style", "list-style-type: circle;"); + }; + + this.finalCheck = function showBullet_finalCheck() { + is(this.liAcc.name, "◦ list element", + "Check that first child of LI is a circle bullet."); + }; + + this.getID = function showBullet_getID() { + return "Show bullet by setting style to circle"; + }; + } + + var gQueue = null; + function doTest() { + var list = getNode("list"); + list.setAttribute("style", "list-style-type: circle;"); + + gQueue = new eventQueue(); + gQueue.push(new hideBullet()); + gQueue.push(new showBullet()); + gQueue.invoke(); // SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1100602" + title="[e10s] crash in mozilla::a11y::ProxyAccessible::Shutdown()"> + Mozilla Bug 1100602 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <ol id="list"> + <li id="list_element">list element</li> + </ol> + +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug1175913.html b/accessible/tests/mochitest/treeupdate/test_bug1175913.html new file mode 100644 index 0000000000..1fe2720434 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug1175913.html @@ -0,0 +1,95 @@ +<html> + +<head> + <title>Test hide/show events on event listener changes</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + function dummyListener() {} + + function testAddListener() { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getNode("parent")), + ]; + + this.invoke = function testAddListener_invoke() { + is(getAccessible("parent", null, null, DONOTFAIL_IF_NO_ACC), null, "Check that parent is not accessible."); + is(getAccessible("child", null, null, DONOTFAIL_IF_NO_ACC), null, "Check that child is not accessible."); + getNode("parent").addEventListener("click", dummyListener); + }; + + this.finalCheck = function testAddListener_finalCheck() { + var tree = { TEXT: [] }; + testAccessibleTree("parent", tree); + }; + + this.getID = function testAddListener_getID() { + return "Test that show event is sent when click listener is added"; + }; + } + + function testRemoveListener() { + this.eventSeq = [ + new unexpectedInvokerChecker(EVENT_HIDE, getNode("parent")), + ]; + + this.invoke = function testRemoveListener_invoke() { + getNode("parent").removeEventListener("click", dummyListener); + }; + + this.finalCheck = function testRemoveListener_finalCheck() { + ok(getAccessible("parent", null, null, DONOTFAIL_IF_NO_ACC), + "Parent stays accessible after click event listener is removed"); + ok(!getAccessible("child", null, null, DONOTFAIL_IF_NO_ACC), + "Child stays inaccessible"); + }; + + this.getID = function testRemoveListener_getID() { + return "Test that hide event is sent when click listener is removed"; + }; + } + + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + gQueue.push(new testAddListener()); + gQueue.push(new testRemoveListener()); + gQueue.invoke(); // SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1175913" + title="Crash in mozilla::a11y::DocAccessibleParent::RemoveAccessible(ProxyAccessible* aAccessible)"> + Mozilla Bug 1175913 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <span id="parent"> + <span id="child"> + </span> + </span> + +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug1189277.html b/accessible/tests/mochitest/treeupdate/test_bug1189277.html new file mode 100644 index 0000000000..6766d08a14 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug1189277.html @@ -0,0 +1,81 @@ +<html> + +<head> + <title>Test hide/show events for HTMLListBulletAccessibles on list restyle</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../name.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function runTest() { + this.containerNode = getNode("outerDiv"); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode("child")), + new invokerChecker(EVENT_HIDE, getNode("childDoc")), + new invokerChecker(EVENT_SHOW, "newChildDoc"), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function runTest_invoke() { + this.containerNode.removeChild(getNode("child")); + + var docContainer = getNode("docContainer"); + var iframe = document.createElement("iframe"); + iframe.setAttribute("src", "http://example.com"); + iframe.setAttribute("id", "newChildDoc"); + + docContainer.removeChild(getNode("childDoc")); + docContainer.appendChild(iframe); + }; + + this.getID = function runTest_getID() { + return "check show events are not incorrectly coalesced"; + }; + } + + // enableLogging("tree"); + gA11yEventDumpToConsole = true; + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + gQueue.push(new runTest()); + gQueue.invoke(); // SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1189277" + title="content process crash caused by missing show event"> + Mozilla Bug 1189277 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="outerDiv"> + <div id="child">foo</div> + <div id="docContainer"> + <iframe id="childDoc" src="about:blank"> + </iframe> + </div> + </div> + +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug1276857.html b/accessible/tests/mochitest/treeupdate/test_bug1276857.html new file mode 100644 index 0000000000..a164247534 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug1276857.html @@ -0,0 +1,131 @@ +<!DOCTYPE html> +<html> + +<head> + <title>DOM mutations test</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function runTest() { + let iframe = document.getElementById("iframe"); + + // children change will recreate the table + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, () => { + let doc = getNode("iframe").contentDocument; + return doc && doc.getElementById("c1"); + }), + ]; + + this.invoke = function runTest_invoke() { + var tree = { + SECTION: [ // c1 + { TEXT_LEAF: [] }, // Some text + { TEXT_CONTAINER: [ + { TEXT_LEAF: [] }, // something with .. + ] }, + { TEXT_LEAF: [] }, // More text + ], + }; + testAccessibleTree(iframe.contentDocument.getElementById("c1"), tree); + + iframe.contentDocument.getElementById("c1_t").querySelector("span").remove(); + }; + + this.finalCheck = function runTest_finalCheck() { + var tree = { + SECTION: [ // c1 + { TEXT_LEAF: [] }, // Some text + { TEXT_LEAF: [] }, // More text + ], + }; + testAccessibleTree(iframe.contentDocument.getElementById("c1"), tree); + }; + + this.getID = function runTest_getID() { + return "child DOM node is removed before the layout notifies the a11y about parent removal/show"; + }; + } + + function runShadowTest() { + // children change will recreate the table + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, () => { + let doc = getNode("iframe").contentDocument; + return doc && doc.getElementById("c2"); + }), + ]; + + this.invoke = function runShadowTest_invoke() { + var tree = { + SECTION: [ // c2 + { TEXT_LEAF: [] }, // Some text + { TEXT_CONTAINER: [ + { TEXT_LEAF: [] }, // something with .. + ] }, + { TEXT_LEAF: [] }, // More text + ], + }; + const iframe = document.getElementById("iframe"); + testAccessibleTree(iframe.contentDocument.getElementById("c2"), tree); + + var shadowRoot = iframe.contentDocument.getElementById("c2_c").shadowRoot; + shadowRoot.firstElementChild.querySelector("span").remove(); + // bug 1487312 + shadowRoot.firstElementChild.offsetTop; + shadowRoot.appendChild(document.createElement("button")); + }; + + this.finalCheck = function runShadowTest_finalCheck() { + var tree = { + SECTION: [ // c2 + { TEXT_LEAF: [] }, // Some text + { TEXT_LEAF: [] }, // More text + { PUSHBUTTON: [] }, // The button we appended. + ], + }; + const iframe = document.getElementById("iframe"); + testAccessibleTree(iframe.contentDocument.getElementById("c2"), tree); + }; + + this.getID = function runShadowTest_getID() { + return "child DOM node is removed before the layout notifies the a11y about parent removal/show in shadow DOM"; + }; + } + + // enableLogging("tree"); + // gA11yEventDumpToConsole = true; + + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + gQueue.push(new runTest()); + gQueue.push(new runShadowTest()); + gQueue.invoke(); // will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + + window.onload = () => { + let iframe = document.createElement("iframe"); + iframe.id = "iframe"; + iframe.src = "test_bug1276857_subframe.html"; + addA11yLoadEvent(doTest, iframe.contentWindow); + document.body.appendChild(iframe); + }; + </script> + +</head> +<body> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug1276857_subframe.html b/accessible/tests/mochitest/treeupdate/test_bug1276857_subframe.html new file mode 100644 index 0000000000..869c9ebe6c --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug1276857_subframe.html @@ -0,0 +1,33 @@ +<!DOCTYPE HTML> +<html> +<head> + <title>DOM mutations test</title> + <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css" /> + <script type="application/javascript" src="../role.js"></script> +</head> +<body> + <div id="c1"> + <div id="c1_t" style="display: table" role="presentation"> + Some text + <span style="display: table-cell">something with accessibles goes here</span> + More text + </div> + </div> + + <template id="tmpl"> + <div style="display: table" role="presentation"> + Some text + <span style="display: table-cell">something with accessibles goes here</span> + More text + </div> + </template> + + <div id="c2"><div id="c2_c" role="presentation"></div></div> + + <script> + var gShadowRoot = document.getElementById("c2_c").attachShadow({mode: "open"}); + var tmpl = document.getElementById("tmpl"); + gShadowRoot.appendChild(document.importNode(tmpl.content, true)); + </script> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug852150.xhtml b/accessible/tests/mochitest/treeupdate/test_bug852150.xhtml new file mode 100644 index 0000000000..51a3c39047 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug852150.xhtml @@ -0,0 +1,57 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <title>Canvas subdom mutation</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + + <script> + <![CDATA[ + function doTest() { + var the_displayNone = getNode("the_displaynone"); + var the_table = getNode("the_table"); + var the_row = getNode("the_row"); + ok(isAccessible(the_table), "table should be accessible"); + the_displayNone.appendChild(the_table); + ok(!isAccessible(the_table), "table in display none tree shouldn't be accessible"); + + setTimeout(function() { + document.body.removeChild(the_row); + // make sure no accessibles have stuck around. + ok(!isAccessible(the_row), "row shouldn't be accessible"); + ok(!isAccessible(the_table), "table shouldn't be accessible"); + ok(!isAccessible(the_displayNone), "display none things shouldn't be accessible"); + SimpleTest.finish(); + }, 0); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> +</head> +<body> + + <a target="_blank" + title="test accessible removal when reframe root isn't accessible" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=852150"> + Mozilla Bug 852150 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <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/mochitest/treeupdate/test_bug883708.xhtml b/accessible/tests/mochitest/treeupdate/test_bug883708.xhtml new file mode 100644 index 0000000000..5d9e813f3a --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug883708.xhtml @@ -0,0 +1,31 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> + +function boom() { + var newSpan = document.createElementNS("http://www.w3.org/1999/xhtml", "span"); + document.getElementById("c").insertBefore(newSpan, document.getElementById("d")); + document.getElementById("a").style.visibility = "visible"; + ok(true, "test didn't crash or assert"); + SimpleTest.finish(); +} + +</script> +</head> + +<body onload="boom();"> + <a target="_blank" + title="test reparenting accessible subtree when inaccessible element becomes accessible" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=883708"> + Mozilla Bug 883708 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + +<div style="visibility: collapse;" id="a"><div style="float: right; visibility: visible;"><div id="c"><td id="d"></td></div></div></div></body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug884251.xhtml b/accessible/tests/mochitest/treeupdate/test_bug884251.xhtml new file mode 100644 index 0000000000..7e3cf14fac --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug884251.xhtml @@ -0,0 +1,19 @@ +<html xmlns="http://www.w3.org/1999/xhtml"> +<head> + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + +<script> + +function boom() { + document.getElementById("k").removeAttribute("href"); + ok(true, "changing iframe contents doesn't cause assertions"); + SimpleTest.finish(); +} + +</script> +</head> + +<body onload="boom();"> +<iframe src="data:text/html,1"><link id="k" href="data:text/html,2" /></iframe> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_bug895082.html b/accessible/tests/mochitest/treeupdate/test_bug895082.html new file mode 100644 index 0000000000..8332c5206e --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_bug895082.html @@ -0,0 +1,49 @@ +<!DOCTYPE html> +<html> +<head> +<title>Replace body test</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> +function doTest() { + var y = document.getElementById("y"); + var oldBody = document.body; + var newBody = document.createElement("body"); + document.documentElement.insertBefore(newBody, oldBody); + setTimeout(function() { + document.documentElement.removeChild(oldBody); + newBody.appendChild(y); + ok(true, "we didn't assert"); + SimpleTest.finish(); + }, 0); +} + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=895082" + title="Bug 895082 - replacing body element asserts"> + Bug 895082</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + +<div><div id="y"></div></div> + +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_canvas.html b/accessible/tests/mochitest/treeupdate/test_canvas.html new file mode 100644 index 0000000000..229bf4f2e3 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_canvas.html @@ -0,0 +1,87 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Canvas subdom mutation</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function addSubtree(aID) { + this.node = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.node), + ]; + + this.invoke = function addSubtree_invoke() { + // ensure we start with no subtree + testAccessibleTree("canvas", { CANVAS: [] }); + getNode("dialog").style.display = "block"; + }; + + this.finalCheck = function addSubtree_finalCheck() { + testAccessibleTree("dialog", { DIALOG: [] }); + }; + + this.getID = function addSubtree_getID() { + return "show canvas subdom"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTest() { + gQueue = new eventQueue(); + + // make the subdom come alive! + gQueue.push(new addSubtree("dialog")); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Expose content in Canvas element" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=495912"> + Mozilla Bug 495912 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <canvas id="canvas"> + <div id="dialog" role="dialog" style="display: none;"> + </div> + </canvas> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_contextmenu.xhtml b/accessible/tests/mochitest/treeupdate/test_contextmenu.xhtml new file mode 100644 index 0000000000..3f91838e59 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_contextmenu.xhtml @@ -0,0 +1,315 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="menu tree and events"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"/> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../events.js" /> + <script type="application/javascript" + src="../role.js" /> + + <script type="application/javascript"> + <![CDATA[ + + function openMenu(aID, aTree) + { + this.eventSeq = [ + new invokerChecker(EVENT_MENUPOPUP_START, getNode(aID)) + ]; + + this.invoke = function openMenu_invoke() + { + var button = getNode("button"); + getNode(aID).openPopup(button, "after_start", 0, 0, true, false); + } + + this.finalCheck = function openMenu_finalCheck(aEvent) + { + testAccessibleTree(aID, aTree); + } + + this.getID = function openMenu_getID() + { + return "open menu " + prettyName(aID); + } + } + + function selectNextMenuItem(aID) + { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getNode(aID)) + ]; + + this.invoke = function selectMenuItem_invoke() + { + synthesizeKey("KEY_ArrowDown"); + } + + this.getID = function selectMenuItem_getID() + { + return "select menuitem " + prettyName(aID); + } + } + + function openSubMenu(aSubMenuID, aItemID, aMenuID, aTree) + { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getNode(aItemID)), + ]; + + this.invoke = function openSubMenu_invoke() + { + synthesizeKey("KEY_Enter"); + } + + this.finalCheck = function openSubMenu_finalCheck(aEvent) + { + testAccessibleTree(aMenuID, aTree); + } + + this.getID = function openSubMenu_getID() + { + return "open submenu " + prettyName(aSubMenuID) + " focusing item " + prettyName(aItemID); + } + } + + function closeSubMenu(aSubMenuID, aItemID) + { + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, getNode(aItemID)), + ]; + + this.invoke = function closeSubMenu_invoke() + { + synthesizeKey("KEY_Escape"); + } + + this.getID = function closeSubMenu_getID() + { + return "close submenu " + prettyName(aSubMenuID) + " focusing item " + prettyName(aItemID); + } + } + + function closeMenu(aID) + { + this.eventSeq = [ + new invokerChecker(EVENT_MENUPOPUP_END, getNode(aID)) + ]; + + this.invoke = function closeMenu_invoke() + { + synthesizeKey("KEY_Escape"); + } + + this.getID = function closeMenu_getID() + { + return "close menu " + prettyName(aID); + } + } + + //gA11yEventDumpToConsole = true; + //enableLogging("tree,verbose"); + + var gQueue = null; + var gContextTree = {}; + + // Linux and Windows menu trees discrepancy: bug 527646. + + /** + * Return the context menu tree before submenus were open. + */ + function getMenuTree1() + { + if (LINUX || SOLARIS) { + let tree = { + role: ROLE_MENUPOPUP, + children: [ + { + name: "item0", + role: ROLE_MENUITEM, + children: [] + }, + { + name: "item1", + role: ROLE_MENUITEM, + children: [] + }, + { + name: "item2", + role: ROLE_PARENT_MENUITEM, + children: [ ] + } + ] + }; + return tree; + } + + // Windows + let tree = { + role: ROLE_MENUPOPUP, + children: [ + { + name: "item0", + role: ROLE_MENUITEM, + children: [] + }, + { + name: "item1", + role: ROLE_MENUITEM, + children: [] + }, + { + name: "item2", + role: ROLE_PARENT_MENUITEM, + children: [ + { + name: "item2", + role: ROLE_MENUPOPUP, + children: [ ] + } + ] + } + ] + }; + return tree; + } + + /** + * Return context menu tree when submenu was open. + */ + function getMenuTree2() + { + var tree = getMenuTree1(); + if (LINUX || SOLARIS) { + let submenuTree = + { + name: "item2.0", + role: ROLE_PARENT_MENUITEM, + children: [ ] + }; + tree.children[2].children.push(submenuTree); + return tree; + } + + // Windows + let submenuTree = + { + name: "item2.0", + role: ROLE_PARENT_MENUITEM, + children: [ + { + name: "item2.0", + role: ROLE_MENUPOPUP, + children: [ ] + } + ] + }; + + tree.children[2].children[0].children.push(submenuTree); + return tree; + } + + /** + * Return context menu tree when subsub menu was open. + */ + function getMenuTree3() + { + var tree = getMenuTree2(); + var subsubmenuTree = + { + name: "item2.0.0", + role: ROLE_MENUITEM, + children: [] + }; + + if (LINUX || SOLARIS) + tree.children[2].children[0].children.push(subsubmenuTree); + else + tree.children[2].children[0].children[0].children[0].children.push(subsubmenuTree); + + return tree; + } + + + function doTests() + { + gQueue = new eventQueue(); + + // Check initial empty tree + testAccessibleTree("context", { MENUPOPUP: [] }); + + // Open context menu and check that menu item accesibles are created. + gQueue.push(new openMenu("context", getMenuTree1())); + + // Select items and check focus event on them. + gQueue.push(new selectNextMenuItem("item0")); + gQueue.push(new selectNextMenuItem("item1")); + gQueue.push(new selectNextMenuItem("item2")); + + // Open sub menu and check menu accessible tree and focus event. + gQueue.push(new openSubMenu("submenu2", "item2.0", + "context", getMenuTree2())); + gQueue.push(new openSubMenu("submenu2.0", "item2.0.0", + "context", getMenuTree3())); + + // Close submenus and check that focus goes to parent. + gQueue.push(new closeSubMenu("submenu2.0", "item2.0")); + gQueue.push(new closeSubMenu("submenu2", "item2")); + + gQueue.push(new closeMenu("context")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=630194" + title="Update accessible tree when opening the menu popup"> + Mozilla Bug 630194 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=630486" + title="Don't force accessible creation for popup children."> + Mozilla Bug 630486 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + + <menupopup id="context"> + <menuitem id="item0" label="item0"/> + <menuitem id="item1" label="item1"/> + <menu id="item2" label="item2"> + <menupopup id="submenu2"> + <menu id="item2.0" label="item2.0"> + <menupopup id="submenu2.0"> + <menuitem id="item2.0.0" label="item2.0.0"/> + </menupopup> + </menu> + </menupopup> + </menu> + </menupopup> + + <button context="context" id="button">btn</button> + </vbox> + </hbox> +</window> diff --git a/accessible/tests/mochitest/treeupdate/test_cssoverflow.html b/accessible/tests/mochitest/treeupdate/test_cssoverflow.html new file mode 100644 index 0000000000..59ab5cae76 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_cssoverflow.html @@ -0,0 +1,150 @@ +<html> + +<head> + <title>Testing HTML scrollable frames (css overflow style)</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + // ////////////////////////////////////////////////////////////////////////// + + /** + * Change scroll range to not empty size and inserts a child into container + * to trigger tree update of the container. Prior to bug 677154 not empty + * size resulted to accessible creation for scroll area, container tree + * update picked up that accessible unattaching scroll area accessible + * subtree. + */ + function changeScrollRange(aContainerID, aScrollAreaID) { + this.containerNode = getNode(aContainerID); + this.container = getAccessible(this.containerNode); + this.scrollAreaNode = getNode(aScrollAreaID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.container), + ]; + + this.invoke = function changeScrollRange_invoke() { + this.scrollAreaNode.style.width = "20px"; + this.containerNode.appendChild(document.createElement("input")); + }; + + this.finalCheck = function changeScrollRange_finalCheck() { + var accTree = + { SECTION: [ // container + { SECTION: [ // scroll area + { ENTRY: [] }, // child content + ] }, + { ENTRY: [] }, // inserted input + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function changeScrollRange_getID() { + return "change scroll range for " + prettyName(aScrollAreaID); + }; + } + + /** + * Change scrollbar styles from hidden to auto to make the scroll area focusable. + * That causes us to create an accessible for it. + * Make sure the tree stays intact. + * The scroll area has no ID on purpose to make it inaccessible initially. + */ + function makeFocusableByScrollbarStyles(aContainerID) { + this.container = getAccessible(aContainerID); + this.scrollAreaNode = getNode(aContainerID).firstChild; + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getAccessible, this.scrollAreaNode), + new invokerChecker(EVENT_REORDER, this.container), + ]; + + this.invoke = function makeFocusableByScrollbarStyles_invoke() { + var accTree = + { SECTION: [ // container + { PARAGRAPH: [ // paragraph + { TEXT_LEAF: [] }, + ] }, + ] }; + testAccessibleTree(this.container, accTree); + + this.scrollAreaNode.style.overflow = "auto"; + }; + + this.finalCheck = function makeFocusableByScrollbarStyles_finalCheck() { + var accTree = + { SECTION: [ // container + { role: ROLE_SECTION, // focusable scroll area + states: STATE_FOCUSABLE, + children: [ + { PARAGRAPH: [ // paragraph + { TEXT_LEAF: [] }, // text leaf + ] }, + ], + }, // focusable scroll area + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function makeFocusableByScrollbarStyles_getID() { + return "make div focusable through scrollbar styles " + + prettyName(aContainerID); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + // ////////////////////////////////////////////////////////////////////////// + + var gQueue = null; + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new changeScrollRange("container", "scrollarea")); + gQueue.push(new makeFocusableByScrollbarStyles("container3")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=677154" + title="Detached document accessibility tree"> + Mozilla Bug 677154</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <div id="container"><div id="scrollarea" style="overflow:auto;"><input></div></div> + <div id="container2"><div id="scrollarea2" style="overflow:hidden;"></div></div> + <div id="container3"><div style="overflow: hidden; height: 1px;"><p>foo</p></div></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_deck.xhtml b/accessible/tests/mochitest/treeupdate/test_deck.xhtml new file mode 100644 index 0000000000..979996a66c --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_deck.xhtml @@ -0,0 +1,154 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Tree update on XUL deck panel switching"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + function switchDeckPanel(aContainerID, aDeckID) + { + this.panelIndex = 0; + + this.container = getAccessible(aContainerID); + this.deckNode = getNode(aDeckID); + this.prevPanel = getAccessible(this.deckNode.selectedPanel); + this.panelNode = this.deckNode.childNodes[this.panelIndex]; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.prevPanel), + new invokerChecker(EVENT_SHOW, this.panelNode), + new invokerChecker(EVENT_REORDER, this.container) + ]; + + this.invoke = function switchDeckPanel_invoke() + { + var tree = + { GROUPING: [ // role="group" + { GROUPING: [ // groupbox, a selected panel #2 + { PUSHBUTTON: [ ] } // button + ] } + ] }; + testAccessibleTree(this.container, tree); + + this.deckNode.selectedIndex = this.panelIndex; + } + + this.finalCheck = function switchDeckPanel_finalCheck() + { + var tree = + { GROUPING: [ // role="group" + { LABEL: [ // description, a selected panel #1 + { TEXT_LEAF: [] } // text leaf, a description value + ] } + ] }; + testAccessibleTree(this.container, tree); + } + + this.getID = function switchDeckPanel_getID() + { + return "switch deck panel"; + } + } + + function showDeckPanel(aContainerID, aPanelID) + { + this.container = getAccessible(aContainerID); + this.deckNode = getNode(aPanelID); + var tree = + { GROUPING: [ // role="group" + { GROUPING: [ // grouping of panel 2 + { PUSHBUTTON: [] } // push button in panel 2 + ] } + ] }; + + + this.unexpectedEventSeq = [ + new invokerChecker(EVENT_REORDER, this.container) + ]; + + this.invoke = function showDeckPanel_invoke() + { + // This stops the refreh driver from doing its regular ticks, and leaves + // us in control. 100 is an arbitrary positive number to advance the clock + // it is not checked or used anywhere. + window.windowUtils.advanceTimeAndRefresh(100); + + testAccessibleTree(this.container, tree); + this.deckNode.style.display = "-moz-box"; + + // This flushes our DOM mutations and forces any pending mutation events. + window.windowUtils.advanceTimeAndRefresh(100); + } + + this.finalCheck = function showDeckPanel_finalCheck() + { + testAccessibleTree(this.container, tree); + + // Return to regular refresh driver ticks. + window.windowUtils.restoreNormalRefresh(); + } + + this.getID = function showDeckPanel_getID() + { + return "show deck panel"; + } + } + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(); + gQueue.push(new showDeckPanel("container", "hidden")); + gQueue.push(new switchDeckPanel("container", "deck")); + gQueue.invoke(); // will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=814836" + title=" xul:deck element messes up screen reader"> + Mozilla Bug 814836 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1" id="container" role="group"> + + <deck id="deck" selectedIndex="1"> + <description>This is the first page</description> + <groupbox> + <button label="This is the second page"/> + </groupbox> + <hbox id="hidden" style="display: none;"><label>This is the third page</label></hbox> + </deck> + + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/treeupdate/test_delayed_removal.html b/accessible/tests/mochitest/treeupdate/test_delayed_removal.html new file mode 100644 index 0000000000..1a3d2c8877 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_delayed_removal.html @@ -0,0 +1,453 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Test accessible delayed removal</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style> + .gentext:before { + content: "START" + } + .gentext:after { + content: "END" + } + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + + async function hideDivFromInsideSpan() { + let msg = "hideDivFromInsideSpan"; + info(msg); + let events = waitForOrderedEvents( + [[EVENT_HIDE, "div1"], [EVENT_REORDER, "span1"]], msg); + document.body.offsetTop; // Flush layout. + getNode("div1").style.display = "none"; + await events; + + testAccessibleTree("c1", { SECTION: [ { REGION: [] }, ] }); + } + + async function showDivFromInsideSpan() { + let msg = "showDivFromInsideSpan"; + info(msg); + let events = waitForOrderedEvents( + [[EVENT_SHOW, "div2"], [EVENT_REORDER, "span2"]], msg); + document.body.offsetTop; // Flush layout. + getNode("div2").style.display = "block"; + await events; + + testAccessibleTree("c2", + { SECTION: [ { REGION: [{ SECTION: [ { TEXT_LEAF: [] } ] }] }, ] }); + } + + async function removeDivFromInsideSpan() { + let msg = "removeDivFromInsideSpan"; + info(msg); + let events = waitForOrderedEvents( + [[EVENT_HIDE, getNode("div3")], [EVENT_REORDER, "span3"]], msg); + document.body.offsetTop; // Flush layout. + getNode("div3").remove(); + await events; + + testAccessibleTree("c3", { SECTION: [ { REGION: [] }, ] }); + } + + // Test to see that generated content is inserted + async function addCSSGeneratedContent() { + let msg = "addCSSGeneratedContent"; + let c4_child = getAccessible("c4_child"); + info(msg); + let events = waitForOrderedEvents([ + [EVENT_SHOW, evt => evt.accessible == c4_child.firstChild], + [EVENT_SHOW, evt => evt.accessible == c4_child.lastChild], + [EVENT_REORDER, c4_child]], msg); + document.body.offsetTop; // Flush layout. + getNode("c4_child").classList.add('gentext'); + await events; + + testAccessibleTree("c4", { SECTION: [ // container + { SECTION: [ // inserted node + { STATICTEXT: [] }, // :before + { TEXT_LEAF: [] }, // primary text + { STATICTEXT: [] }, // :after + ] }, + ] }); + } + + // Test to see that generated content gets removed + async function removeCSSGeneratedContent() { + let msg = "removeCSSGeneratedContent"; + let c5_child = getAccessible("c5_child"); + info(msg); + let events = waitForEvents([ + [EVENT_HIDE, c5_child.firstChild], + [EVENT_HIDE, c5_child.lastChild], + [EVENT_REORDER, c5_child]], msg); + document.body.offsetTop; // Flush layout. + getNode("c5_child").classList.remove('gentext'); + await events; + + testAccessibleTree("c5",{ SECTION: [ // container + { SECTION: [ // inserted node + { TEXT_LEAF: [] }, // primary text + ] }, + ] }); + } + + // Test to see that a non-accessible intermediate container gets its accessible + // descendants removed and inserted correctly. + async function intermediateNonAccessibleContainers() { + let msg = "intermediateNonAccessibleContainers"; + info(msg); + + testAccessibleTree("c6",{ SECTION: [ + { SECTION: [ + { role: ROLE_PUSHBUTTON, name: "Hello" }, + ] }, + ] }); + + let events = waitForOrderedEvents( + [[EVENT_HIDE, "b1"], [EVENT_SHOW, "b2"], [EVENT_REORDER, "scrollarea"]], msg); + document.body.offsetTop; // Flush layout. + getNode("scrollarea").style.overflow = "auto"; + document.querySelector("#scrollarea > div > div:first-child").style.display = "none"; + document.querySelector("#scrollarea > div > div:last-child").style.display = "block"; + await events; + + testAccessibleTree("c6",{ SECTION: [ + { SECTION: [ + { role: ROLE_PUSHBUTTON, name: "Goodbye" }, + ] }, + ] }); + } + + // Test to see that the button gets reparented into the new accessible container. + async function intermediateNonAccessibleContainerBecomesAccessible() { + let msg = "intermediateNonAccessibleContainerBecomesAccessible"; + info(msg); + + testAccessibleTree("c7",{ SECTION: [ + { role: ROLE_PUSHBUTTON, name: "Hello" }, + { TEXT_LEAF: [] } + ] }); + + let events = waitForOrderedEvents( + [[EVENT_HIDE, "b3"], + // b3 show event coalesced into its new container + [EVENT_SHOW, evt => evt.DOMNode.classList.contains('intermediate')], + [EVENT_REORDER, "c7"]], msg); + document.body.offsetTop; // Flush layout. + document.querySelector("#c7 > div").style.display = "block"; + await events; + + testAccessibleTree("c7",{ SECTION: [ + { SECTION: [ { role: ROLE_PUSHBUTTON, name: "Hello" } ] } + ] }); + } + + // Test to ensure that relocated accessibles are removed when a DOM + // ancestor is hidden. + async function removeRelocatedWhenDomAncestorHidden() { + info("removeRelocatedWhenDomAncestorHidden"); + + testAccessibleTree("c8",{ SECTION: [ + { EDITCOMBOBOX: [ // c8_owner + { COMBOBOX_LIST: [] }, // c8_owned + ]}, + { SECTION: [] }, // c8_owned_container + ] }); + + let events = waitForOrderedEvents([ + [EVENT_HIDE, "c8_owned_container"], + [EVENT_HIDE, "c8_owned"], + [EVENT_REORDER, "c8"], + ], "removeRelocatedWhenDomAncestorHidden"); + document.body.offsetTop; // Flush layout. + getNode("c8_owned_container").hidden = true; + await events; + + testAccessibleTree("c8",{ SECTION: [ + { EDITCOMBOBOX: [] }, // c8_owner + ] }); + } + + // Bug 1572829 + async function removeShadowRootHost() { + info("removeShadowRootHost"); + document.body.offsetTop; // Flush layout. + + let event = waitForEvent(EVENT_REORDER, "c9", "removeShadowRootHost"); + getNode("c9").firstElementChild.attachShadow({mode: "open"}); + getNode("c9").firstElementChild.replaceWith(""); + + await event; + } + + function listItemReframe() { + testAccessibleTree("li",{ LISTITEM: [ + { LISTITEM_MARKER: [] }, + { TEXT_LEAF: [] }, + ] }); + + getNode("li").style.listStylePosition = "inside"; + document.body.offsetTop; // Flush layout. + window.windowUtils.advanceTimeAndRefresh(100); + + testAccessibleTree("li",{ LISTITEM: [ + { LISTITEM_MARKER: [] }, + { TEXT_LEAF: [] }, + ] }); + + window.windowUtils.restoreNormalRefresh(); + } + + // Check to see that a reframed body gets its children pruned correctly. + async function bodyReframe(argument) { + // Load sub-document in iframe. + let event = waitForEvent(EVENT_REORDER, "iframe", "bodyReframe"); + getNode("iframe").src = + `data:text/html,<div>Hello</div><div style="display: none">World</div>`; + await event; + + // Initial tree should have one section leaf. + testAccessibleTree("c10",{ SECTION: [ + { INTERNAL_FRAME: [ + { DOCUMENT: [ + { SECTION: [ + { role: ROLE_TEXT_LEAF, name: "Hello" } + ] } + ]} + ] } + ] }); + + + let iframeDoc = getNode("iframe").contentWindow.document; + + // Trigger coalesced reframing. Both the body node and its children + // will need reframing. + event = waitForEvent(EVENT_REORDER, iframeDoc, "bodyReframe"); + iframeDoc.body.style.display = "inline-block"; + iframeDoc.querySelector("div:first-child").style.display = "none"; + iframeDoc.querySelector("div:last-child").style.display = "block"; + + await event; + + // Only the second section should be showing + testAccessibleTree("c10",{ SECTION: [ + { INTERNAL_FRAME: [ + { DOCUMENT: [ + { SECTION: [ + { role: ROLE_TEXT_LEAF, name: "World" } + ] } + ]} + ] } + ] }); + } + + // Ensure that embed elements recreate their Accessible if they started + // without an src and then an src is set later. + async function embedBecomesOuterDoc() { + let msg = "embedBecomesOuterDoc"; + info(msg); + + testAccessibleTree("c12", { SECTION: [ + { TEXT: [] } + ] }); + + let events = waitForOrderedEvents([ + [EVENT_HIDE, "embed"], + [EVENT_SHOW, "embed"], + [EVENT_REORDER, "c12"], + ], msg); + getNode("embed").src = "data:text/html,"; + await events; + + testAccessibleTree("c12", { SECTION: [ + { INTERNAL_FRAME: [ + { DOCUMENT: [] } + ] } + ] }); + } + + // Test that we get a text removed event when removing generated content from a button + async function testCSSGeneratedContentRemovedFromButton() { + let msg = "testCSSGeneratedContentRemovedFromButton"; + info(msg); + + testAccessibleTree("c13", { SECTION: [ + { role: ROLE_PUSHBUTTON, name: "beforego", + children: [{ STATICTEXT: [] }, { TEXT_LEAF: [] }] } + ] }); + + let events = waitForOrderedEvents([ + [EVENT_HIDE, evt => evt.accessible.name == "before"], + [EVENT_TEXT_REMOVED, evt => evt.accessible.role == ROLE_PUSHBUTTON], + [EVENT_SHOW, evt => evt.DOMNode.tagName == "HR"], + [EVENT_REORDER, "c13"], + ], msg); + getNode("b13").click(); + await events; + + testAccessibleTree("c13", { SECTION: [ + { role: ROLE_PUSHBUTTON, name: "go", + children: [{ TEXT_LEAF: [] }] }, + { SEPARATOR: [] } + ] }); + } + + // Slack seems to often restyle containers and change children + // simultaneously, this results in an insertion queue filled with + // redundant insertions and unparented nodes. + // This test duplicates some of this. + async function testSlack() { + let msg = "testSlack"; + info(msg); + + window.windowUtils.advanceTimeAndRefresh(100); + let event = waitForEvent(EVENT_REORDER, "c14", "testSlack"); + + let keyContainer = document.querySelector("#c14 .intermediate"); + keyContainer.style.display = "inline-block"; + document.body.offsetTop; // Flush layout. + + let one = document.querySelector("#c14 [aria-label='one']"); + let three = document.querySelector("#c14 [aria-label='three']"); + one.remove(); + three.remove(); + // insert one first + keyContainer.firstChild.before(one.cloneNode()); + // insert three last + keyContainer.lastChild.after(three.cloneNode()); + + keyContainer.style.display = "flex"; + document.body.offsetTop; // Flush layout. + + window.windowUtils.restoreNormalRefresh(); + + await event; + + is(getAccessible("c14").name, "one two three", "subtree has correct order"); + } + + async function doTest() { + await hideDivFromInsideSpan(); + + await showDivFromInsideSpan(); + + await removeDivFromInsideSpan(); + + await addCSSGeneratedContent(); + + await removeCSSGeneratedContent(); + + await intermediateNonAccessibleContainers(); + + await intermediateNonAccessibleContainerBecomesAccessible(); + + await removeRelocatedWhenDomAncestorHidden(); + + await removeShadowRootHost(); + + listItemReframe(); + + await bodyReframe(); + + await embedBecomesOuterDoc(); + + await testCSSGeneratedContentRemovedFromButton(); + + await testSlack(); + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="c1"> + <span role="region" id="span1" aria-label="region"><div id="div1">hello</div></span> + </div> + + <div id="c2"> + <span role="region" id="span2" aria-label="region"><div id="div2" style="display: none">hello</div></span> + </div> + + <div id="c3"> + <span role="region" id="span3" aria-label="region"><div id="div3">hello</div></span> + </div> + + <div id="c4"><div id="c4_child">text</div></div> + + <div id="c5"><div id="c5_child" class="gentext">text</div></div> + + <div id="c6"> + <div id="scrollarea" style="overflow:hidden;"> + <div><div role="none"><button id="b1">Hello</button></div><div role="none" style="display: none"><button id="b2">Goodbye</button></div></div> + </div> + </div> + + <div id="c7"> + <div style="display: inline;" class="intermediate"> + <button id="b3">Hello</button> + </div> + </div> + + <div id="c8"> + <div id="c8_owner" role="combobox" aria-owns="c8_owned"></div> + <div id="c8_owned_container"> + <div id="c8_owned" role="listbox"></div> + </div> + </div> + + <div id="c9"> + <div><dir>a</dir></div> + </div> + + <div id="c11"> + <ul> + <li id="li">Test</li> + </ul> + </div> + + <div id="c12"><embed id="embed"></embed></div> + + <div id="c10"> + <iframe id="iframe"></iframe> + </div> + + <div id="c13"> + <style> + .before::before { content: 'before' } + </style> + <button id="b13" class="before" onclick="this.className = ''; this.insertAdjacentElement('afterend', document.createElement('hr'))">go</button> + </div> + + <div role="heading" id="c14" data-qa="virtual-list-item"> + <div class="intermediate"> + <div role="img" aria-label="one"></div> two <div role="img" + aria-label="three"></div> + </div> + </div> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_doc.html b/accessible/tests/mochitest/treeupdate/test_doc.html new file mode 100644 index 0000000000..6bb2863df4 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_doc.html @@ -0,0 +1,415 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Test document root content mutations</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../states.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Helpers + + function getDocNode(aID) { + return getNode(aID).contentDocument; + } + function getDocChildNode(aID) { + return getDocNode(aID).body.firstChild; + } + + function rootContentReplaced(aID, aTextName, aRootContentRole) { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getDocChildNode, aID), + new invokerChecker(EVENT_REORDER, getDocNode, aID), + ]; + + this.finalCheck = function rootContentReplaced_finalCheck() { + var tree = { + role: aRootContentRole || ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: aTextName, + }, + ], + }; + testAccessibleTree(getDocNode(aID), tree); + }; + } + + function rootContentRemoved(aID) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, null), + new invokerChecker(EVENT_REORDER, getDocNode, aID), + ]; + + this.preinvoke = function rootContentRemoved_preinvoke() { + // Set up target for hide event before we invoke. + var text = getAccessible(getAccessible(getDocNode(aID)).firstChild); + this.eventSeq[0].target = text; + }; + + this.finalCheck = function rootContentRemoved_finalCheck() { + var tree = { + role: ROLE_DOCUMENT, + children: [ ], + }; + testAccessibleTree(getDocNode(aID), tree); + }; + } + + function rootContentInserted(aID, aTextName) { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getDocChildNode, aID), + new invokerChecker(EVENT_REORDER, getDocNode, aID), + ]; + + this.finalCheck = function rootContentInserted_finalCheck() { + var tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_TEXT_LEAF, + name: aTextName, + }, + ], + }; + testAccessibleTree(getDocNode(aID), tree); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function writeIFrameDoc(aID) { + this.__proto__ = new rootContentReplaced(aID, "hello"); + + this.invoke = function writeIFrameDoc_invoke() { + var docNode = getDocNode(aID); + + // We can't use open/write/close outside of iframe document because of + // security error. + var script = docNode.createElement("script"); + script.textContent = "document.open(); document.write('hello'); document.close();"; + docNode.body.appendChild(script); + }; + + this.getID = function writeIFrameDoc_getID() { + return "write document"; + }; + } + + /** + * Replace HTML element. + */ + function replaceIFrameHTMLElm(aID) { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getDocChildNode, aID), + new invokerChecker(EVENT_REORDER, getDocNode, aID), + ]; + + this.invoke = function replaceIFrameHTMLElm_invoke() { + var docNode = getDocNode(aID); + var newHTMLNode = docNode.createElement("html"); + newHTMLNode.innerHTML = `<body><p>New Wave</p></body`; + docNode.replaceChild(newHTMLNode, docNode.documentElement); + }; + + this.finalCheck = function replaceIFrameHTMLElm_finalCheck() { + var tree = { + role: ROLE_DOCUMENT, + children: [ + { + role: ROLE_PARAGRAPH, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "New Wave", + }, + ], + }, + ], + }; + testAccessibleTree(getDocNode(aID), tree); + }; + + this.getID = function replaceIFrameHTMLElm_getID() { + return "replace HTML element"; + }; + } + + /** + * Replace HTML body on new body having ARIA role. + */ + function replaceIFrameBody(aID) { + this.__proto__ = new rootContentReplaced(aID, "New Hello"); + + this.invoke = function replaceIFrameBody_invoke() { + var docNode = getDocNode(aID); + var newBodyNode = docNode.createElement("body"); + var newTextNode = docNode.createTextNode("New Hello"); + newBodyNode.appendChild(newTextNode); + docNode.documentElement.replaceChild(newBodyNode, docNode.body); + }; + + this.getID = function replaceIFrameBody_getID() { + return "replace body"; + }; + } + + /** + * Replace HTML body on new body having ARIA role. + */ + function replaceIFrameBodyOnARIARoleBody(aID) { + this.__proto__ = new rootContentReplaced(aID, "New Hello", + ROLE_APPLICATION); + + this.invoke = function replaceIFrameBodyOnARIARoleBody_invoke() { + var docNode = getDocNode(aID); + var newBodyNode = docNode.createElement("body"); + var newTextNode = docNode.createTextNode("New Hello"); + newBodyNode.appendChild(newTextNode); + newBodyNode.setAttribute("role", "application"); + docNode.documentElement.replaceChild(newBodyNode, docNode.body); + }; + + this.getID = function replaceIFrameBodyOnARIARoleBody_getID() { + return "replace body on body having ARIA role"; + }; + } + + /** + * Open/close document pair. + */ + function openIFrameDoc(aID) { + this.__proto__ = new rootContentRemoved(aID); + + this.invoke = function openIFrameDoc_invoke() { + this.preinvoke(); + + // Open document. + var docNode = getDocNode(aID); + var script = docNode.createElement("script"); + script.textContent = "function closeMe() { document.write('Works?'); document.close(); } window.closeMe = closeMe; document.open();"; + docNode.body.appendChild(script); + }; + + this.getID = function openIFrameDoc_getID() { + return "open document"; + }; + } + + function closeIFrameDoc(aID) { + this.__proto__ = new rootContentInserted(aID, "Works?"); + + this.invoke = function closeIFrameDoc_invoke() { + // Write and close document. + getDocNode(aID).write("Works?"); getDocNode(aID).close(); + }; + + this.getID = function closeIFrameDoc_getID() { + return "close document"; + }; + } + + /** + * Remove/insert HTML element pair. + */ + function removeHTMLFromIFrameDoc(aID) { + this.__proto__ = new rootContentRemoved(aID); + + this.invoke = function removeHTMLFromIFrameDoc_invoke() { + this.preinvoke(); + + // Remove HTML element. + var docNode = getDocNode(aID); + docNode.firstChild.remove(); + }; + + this.getID = function removeHTMLFromIFrameDoc_getID() { + return "remove HTML element"; + }; + } + + function insertHTMLToIFrameDoc(aID) { + this.__proto__ = new rootContentInserted(aID, "Haha"); + + this.invoke = function insertHTMLToIFrameDoc_invoke() { + // Insert HTML element. + var docNode = getDocNode(aID); + var html = docNode.createElement("html"); + var body = docNode.createElement("body"); + var text = docNode.createTextNode("Haha"); + body.appendChild(text); + html.appendChild(body); + docNode.appendChild(html); + }; + + this.getID = function insertHTMLToIFrameDoc_getID() { + return "insert HTML element document"; + }; + } + + /** + * Remove/insert HTML body pair. + */ + function removeBodyFromIFrameDoc(aID) { + this.__proto__ = new rootContentRemoved(aID); + + this.invoke = function removeBodyFromIFrameDoc_invoke() { + this.preinvoke(); + + // Remove body element. + var docNode = getDocNode(aID); + docNode.documentElement.removeChild(docNode.body); + }; + + this.getID = function removeBodyFromIFrameDoc_getID() { + return "remove body element"; + }; + } + + function insertElmUnderDocElmWhileBodyMissed(aID) { + this.docNode = null; + this.inputNode = null; + + function getInputNode() { return this.inputNode; } + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getInputNode.bind(this)), + new invokerChecker(EVENT_REORDER, getDocNode, aID), + ]; + + this.invoke = function invoke() { + this.docNode = getDocNode(aID); + this.inputNode = this.docNode.createElement("input"); + this.docNode.documentElement.appendChild(this.inputNode); + }; + + this.finalCheck = function finalCheck() { + var tree = + { DOCUMENT: [ + { ENTRY: [ ] }, + ] }; + testAccessibleTree(this.docNode, tree); + + // Remove aftermath of this test before next test starts. + this.docNode.documentElement.removeChild(this.inputNode); + }; + + this.getID = function getID() { + return "Insert element under document element while body is missed."; + }; + } + + function insertBodyToIFrameDoc(aID) { + this.__proto__ = new rootContentInserted(aID, "Yo ho ho i butylka roma!"); + + this.invoke = function insertBodyToIFrameDoc_invoke() { + // Insert body element. + var docNode = getDocNode(aID); + var body = docNode.createElement("body"); + var text = docNode.createTextNode("Yo ho ho i butylka roma!"); + body.appendChild(text); + docNode.documentElement.appendChild(body); + }; + + this.getID = function insertBodyToIFrameDoc_getID() { + return "insert body element"; + }; + } + + function changeSrc(aID) { + this.containerNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function changeSrc_invoke() { + this.containerNode.src = "data:text/html,<html><input></html>"; + }; + + this.finalCheck = function changeSrc_finalCheck() { + var tree = + { INTERNAL_FRAME: [ + { DOCUMENT: [ + { ENTRY: [ ] }, + ] }, + ] }; + testAccessibleTree(this.containerNode, tree); + }; + + this.getID = function changeSrc_getID() { + return "change src on iframe"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpToConsole = true; + // enableLogging('tree,verbose'); + + var gQueue = null; + + function doTest() { + gQueue = new eventQueue(); + + gQueue.push(new writeIFrameDoc("iframe")); + gQueue.push(new replaceIFrameHTMLElm("iframe")); + gQueue.push(new replaceIFrameBody("iframe")); + gQueue.push(new openIFrameDoc("iframe")); + gQueue.push(new closeIFrameDoc("iframe")); + gQueue.push(new removeHTMLFromIFrameDoc("iframe")); + gQueue.push(new insertHTMLToIFrameDoc("iframe")); + gQueue.push(new removeBodyFromIFrameDoc("iframe")); + gQueue.push(new insertElmUnderDocElmWhileBodyMissed("iframe")); + gQueue.push(new insertBodyToIFrameDoc("iframe")); + gQueue.push(new changeSrc("iframe")); + gQueue.push(new replaceIFrameBodyOnARIARoleBody("iframe")); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Update accessible tree when root element is changed" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=606082">Mozilla Bug 606082</a> + <a target="_blank" + title="Elements inserted outside the body aren't accessible" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=608887">Mozilla Bug 608887</a> + <a target="_blank" + title="Reorder event for document must be fired after document initial tree creation" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=669263">Mozilla Bug 669263</a> + <a target="_blank" + title="Changing the HTML body doesn't pick up ARIA role" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=818407">Mozilla Bug 818407</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <iframe id="iframe"></iframe> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_gencontent.html b/accessible/tests/mochitest/treeupdate/test_gencontent.html new file mode 100644 index 0000000000..9a0c107133 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_gencontent.html @@ -0,0 +1,187 @@ +<html> + +<head> + <title>Elements with CSS generated content</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style> + .gentext:before { + content: "START" + } + .gentext:after { + content: "END" + } + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + // ////////////////////////////////////////////////////////////////////////// + + /** + * Insert new node with CSS generated content style applied to container. + */ + function insertNodeHavingGenContent(aContainerID) { + this.containerNode = getNode(aContainerID); + this.container = getAccessible(this.containerNode); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getFirstChild, this.container), + new invokerChecker(EVENT_REORDER, this.container), + ]; + + this.invoke = function insertNodeHavingGenContent_invoke() { + var node = document.createElement("div"); + node.textContent = "text"; + node.setAttribute("class", "gentext"); + this.containerNode.appendChild(node); + }; + + this.finalCheck = function insertNodeHavingGenContent_finalCheck() { + var accTree = + { SECTION: [ // container + { SECTION: [ // inserted node + { STATICTEXT: [] }, // :before + { TEXT_LEAF: [] }, // primary text + { STATICTEXT: [] }, // :after + ] }, + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function insertNodeHavingGenContent_getID() { + return "insert node having generated content to " + prettyName(aContainerID); + }; + } + + /** + * Add CSS generated content to the given node contained by container node. + */ + function addGenContent(aContainerID, aNodeID) { + this.container = getAccessible(aContainerID); + this.nodeAcc = getAccessible(aNodeID); + this.node = getNode(aNodeID); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getFirstChild, this.nodeAcc), + new invokerChecker(EVENT_SHOW, getLastChild, this.nodeAcc), + new invokerChecker(EVENT_REORDER, this.nodeAcc), + ]; + + this.invoke = function addGenContent_invoke() { + this.node.classList.add("gentext"); + }; + + this.finalCheck = function insertNodeHavingGenContent_finalCheck() { + var accTree = + { SECTION: [ // container + { SECTION: [ // inserted node + { STATICTEXT: [] }, // :before + { TEXT_LEAF: [] }, // primary text + { STATICTEXT: [] }, // :after + ] }, + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function addGenContent_getID() { + return "add generated content to" + prettyName(aNodeID); + }; + } + + /** + * Remove CSS generated content from the given node contained by container node. + */ + function removeGenContent(aContainerID, aNodeID) { + this.container = getAccessible(aContainerID); + this.nodeAcc = getAccessible(aNodeID); + this.node = getNode(aNodeID); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.nodeAcc.lastChild), + new invokerChecker(EVENT_HIDE, this.nodeAcc.firstChild), + new invokerChecker(EVENT_REORDER, this.nodeAcc), + ]; + + this.invoke = function removeGenContent_invoke() { + this.node.classList.remove("gentext"); + }; + + this.finalCheck = function removeGenContent_finalCheck() { + var accTree = + { SECTION: [ // container + { SECTION: [ // inserted node + { TEXT_LEAF: [] }, // primary text + ] }, + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function addGenContent_getID() { + return "remove generated content from" + prettyName(aNodeID); + }; + } + /** + * Target getters. + */ + function getFirstChild(aAcc) { + try { return aAcc.firstChild; } catch (e) { return null; } + } + + function getLastChild(aAcc) { + try { return aAcc.lastChild; } catch (e) { return null; } + } + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + // ////////////////////////////////////////////////////////////////////////// + + var gQueue = null; + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new insertNodeHavingGenContent("container1")); + gQueue.push(new addGenContent("container2", "container2_child")); + gQueue.push(new removeGenContent("container3", "container3_child")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=646350" + title="Add a test for dynamic chnages of CSS generated content"> + Mozilla Bug 646350</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="eventdump"></div> + + <div id="container1"></div> + <div id="container2"><div id="container2_child">text</div></div> + <div id="container3"><div id="container3_child" class="gentext">text</div></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_general.html b/accessible/tests/mochitest/treeupdate/test_general.html new file mode 100644 index 0000000000..8129cae98a --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_general.html @@ -0,0 +1,174 @@ +<html> + +<head> + <title>Testing the tree updates</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + // ////////////////////////////////////////////////////////////////////////// + + function prependAppend(aContainer) { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, aContainer), + ]; + + this.invoke = function prependAppend_invoke() { + var checkbox = document.createElement("input"); + checkbox.setAttribute("type", "checkbox"); + getNode(aContainer).insertBefore(checkbox, getNode(aContainer).firstChild); + + var button = document.createElement("input"); + button.setAttribute("type", "button"); + getNode(aContainer).appendChild(button); + }; + + this.finalCheck = function prependAppend_finalCheck() { + var accTree = + { SECTION: [ // container + { CHECKBUTTON: [ ] }, + { ENTRY: [ ] }, + { PUSHBUTTON: [ ] }, + ] }; + testAccessibleTree(aContainer, accTree); + }; + + this.getID = function prependAppend_getID() { + return "prepends a child and appends a child"; + }; + } + + function removeRemove(aContainer) { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, aContainer), + ]; + + this.invoke = function removeRemove_invoke() { + getNode(aContainer).firstChild.remove(); + }; + + this.finalCheck = function removeRemove_finalCheck() { + var accTree = + { SECTION: [ // container + { PUSHBUTTON: [ ] }, + ] }; + testAccessibleTree(aContainer, accTree); + }; + + this.getID = function removeRemove_getID() { + return "remove first and second children"; + }; + } + + function insertInaccessibleAccessibleSiblings() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, "c3"), + ]; + + this.invoke = function insertInaccessibleAccessibleSiblings_invoke() { + getNode("c3").appendChild(document.createElement("span")); + getNode("c3").appendChild(document.createElement("input")); + }; + + this.finalCheck = function insertInaccessibleAccessibleSiblings_finalCheck() { + var accTree = + { SECTION: [ // container + { PUSHBUTTON: [ + { TEXT_LEAF: [] }, + ] }, + { ENTRY: [ ] }, + ] }; + testAccessibleTree("c3", accTree); + }; + + this.getID = function insertInaccessibleAccessibleSiblings_getID() { + return "insert inaccessible and then accessible siblings"; + }; + } + + // Test for bug 1500416. + function displayContentsInsertion() { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, "c4"), + ]; + + this.invoke = function displayContentsInsertion_invoke() { + document.body.offsetTop; // Flush layout. + + let list = document.createElement("ul"); + list.style.display = "contents"; + list.appendChild(document.createElement("li")); + list.firstChild.appendChild(document.createTextNode("Text")); + getNode("c4").appendChild(list); + }; + + this.finalCheck = function displayContentsInsertion_finalCheck() { + var accTree = + { SECTION: [ // container + { LIST: [ + { LISTITEM: [ + { LISTITEM_MARKER: [] }, + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }; + testAccessibleTree("c4", accTree); + }; + + this.getID = function displayContentsInsertion_getID() { + return "insert accessible display: contents element."; + }; + } + + + // ////////////////////////////////////////////////////////////////////////// + // Do tests + // ////////////////////////////////////////////////////////////////////////// + + var gQueue = null; + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + function doTests() { + gQueue = new eventQueue(); + + gQueue.push(new prependAppend("c1")); + gQueue.push(new removeRemove("c2")); + gQueue.push(new insertInaccessibleAccessibleSiblings()); + gQueue.push(new displayContentsInsertion()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTests); + </script> +</head> + +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="c1"><input></div> + <div id="c2"><span><input type="checkbox"><input></span><input type="button"></div> + + <div id="c3"><input type="button" value="button"></div> + <div id="c4"></div> + +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_hidden.html b/accessible/tests/mochitest/treeupdate/test_hidden.html new file mode 100644 index 0000000000..e687fc97c9 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_hidden.html @@ -0,0 +1,125 @@ +<!DOCTYPE html> +<html> + +<head> + <title>@hidden attribute testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + // ////////////////////////////////////////////////////////////////////////// + + /** + * Set @hidden attribute + */ + function setHiddenAttr(aContainerID, aChildID) { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getNode(aContainerID)), + ]; + + this.invoke = function setHiddenAttr_invoke() { + var tree = + { SECTION: [ + { ENTRY: [ + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + + getNode(aChildID).setAttribute("hidden", "true"); + }; + + this.finalCheck = function setHiddenAttr_finalCheck() { + var tree = + { SECTION: [ + ] }; + testAccessibleTree(aContainerID, tree); + }; + + this.getID = function setHiddenAttr_getID() { + return "Set @hidden attribute on input and test accessible tree for div"; + }; + } + + /** + * Remove @hidden attribute + */ + function removeHiddenAttr(aContainerID, aChildID) { + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, getNode(aContainerID)), + ]; + + this.invoke = function removeHiddenAttr_invoke() { + var tree = + { SECTION: [ + ] }; + testAccessibleTree(aContainerID, tree); + + getNode(aChildID).removeAttribute("hidden"); + }; + + this.finalCheck = function removeHiddenAttr_finalCheck() { + var tree = + { SECTION: [ + { ENTRY: [ + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + }; + + this.getID = function removeHiddenAttr_getID() { + return "Remove @hidden attribute on input and test accessible tree for div"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + // ////////////////////////////////////////////////////////////////////////// + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTest() { + gQueue = new eventQueue(); + + gQueue.push(new setHiddenAttr("container", "child")); + gQueue.push(new removeHiddenAttr("container", "child")); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + + </script> + +</head> + +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="container"><input id="child"></div> + + <div id="eventdump"></div> + +</body> + +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_imagemap.html b/accessible/tests/mochitest/treeupdate/test_imagemap.html new file mode 100644 index 0000000000..b401b54eff --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_imagemap.html @@ -0,0 +1,399 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML img map accessible tree update tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + function insertArea(aImageMapID, aMapID) { + this.imageMap = getAccessible(aImageMapID); + this.mapNode = getNode(aMapID); + + function getInsertedArea(aThisObj) { + return aThisObj.imageMap.firstChild; + } + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getInsertedArea, this), + new invokerChecker(EVENT_REORDER, this.imageMap), + ]; + + this.invoke = function insertArea_invoke() { + var areaElm = document.createElement("area"); + 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"); + + this.mapNode.insertBefore(areaElm, this.mapNode.firstChild); + }; + + this.finalCheck = function insertArea_finalCheck() { + var accTree = + { IMAGE_MAP: [ + { + role: ROLE_LINK, + name: "a", + children: [ ], + }, + { + role: ROLE_LINK, + name: "b", + children: [ ], + }, + ] }; + testAccessibleTree(this.imageMap, accTree); + }; + + this.getID = function insertArea_getID() { + return "insert area element"; + }; + } + + function appendArea(aImageMapID, aMapID) { + this.imageMap = getAccessible(aImageMapID); + this.mapNode = getNode(aMapID); + + function getAppendedArea(aThisObj) { + return aThisObj.imageMap.lastChild; + } + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getAppendedArea, this), + new invokerChecker(EVENT_REORDER, this.imageMap), + ]; + + this.invoke = function appendArea_invoke() { + var areaElm = document.createElement("area"); + 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"); + + this.mapNode.appendChild(areaElm); + }; + + this.finalCheck = function appendArea_finalCheck() { + var accTree = + { IMAGE_MAP: [ + { + role: ROLE_LINK, + name: "a", + children: [ ], + }, + { + role: ROLE_LINK, + name: "b", + children: [ ], + }, + { + role: ROLE_LINK, + name: "c", + children: [ ], + }, + ] }; + testAccessibleTree(this.imageMap, accTree); + }; + + this.getID = function appendArea_getID() { + return "append area element"; + }; + } + + function removeArea(aImageMapID, aMapID) { + this.imageMap = getAccessible(aImageMapID); + this.area = null; + this.mapNode = getNode(aMapID); + + function getRemovedArea(aThisObj) { + return aThisObj.area; + } + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getRemovedArea, this), + new invokerChecker(EVENT_REORDER, this.imageMap), + ]; + + this.invoke = function removeArea_invoke() { + this.area = this.imageMap.firstChild; + this.mapNode.removeChild(this.mapNode.firstElementChild); + }; + + this.finalCheck = function removeArea_finalCheck() { + var accTree = + { IMAGE_MAP: [ + { + role: ROLE_LINK, + name: "b", + children: [ ], + }, + { + role: ROLE_LINK, + name: "c", + children: [ ], + }, + ] }; + testAccessibleTree(this.imageMap, accTree); + }; + + this.getID = function removeArea_getID() { + return "remove area element"; + }; + } + + function removeNameOnMap(aImageMapContainerID, aImageMapID, aMapID) { + this.container = getAccessible(aImageMapContainerID); + this.containerNode = this.container.DOMNode; + this.imageMap = getAccessible(aImageMapID); + this.imgNode = this.imageMap.DOMNode; + this.mapNode = getNode(aMapID); + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.imageMap), + new invokerChecker(EVENT_SHOW, this.imgNode), + new invokerChecker(EVENT_REORDER, this.container), + ]; + + this.invoke = function removeNameOnMap_invoke() { + this.mapNode.removeAttribute("name"); + }; + + this.finalCheck = function removeNameOnMap_finalCheck() { + var accTree = + { SECTION: [ + { GRAPHIC: [ ] }, + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function removeNameOnMap_getID() { + return "remove @name on map element"; + }; + } + + function restoreNameOnMap(aImageMapContainerID, aImageMapID, aMapID) { + this.container = getAccessible(aImageMapContainerID); + this.containerNode = this.container.DOMNode; + this.imageMap = null; + this.imgNode = getNode(aImageMapID); + this.mapNode = getNode(aMapID); + + function getImageMap(aThisObj) { + return aThisObj.imageMap; + } + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getImageMap, this), + new invokerChecker(EVENT_SHOW, this.imgNode), + new invokerChecker(EVENT_REORDER, this.container), + ]; + + this.invoke = function restoreNameOnMap_invoke() { + this.imageMap = getAccessible(aImageMapID); + this.mapNode.setAttribute("name", "atoz_map"); + + // XXXhack: force repainting of the image (see bug 745788 for details). + waveOverImageMap(aImageMapID); + }; + + this.finalCheck = function removeNameOnMap_finalCheck() { + var accTree = + { SECTION: [ + { IMAGE_MAP: [ + { LINK: [ ] }, + { LINK: [ ] }, + ] }, + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function removeNameOnMap_getID() { + return "restore @name on map element"; + }; + } + + function removeMap(aImageMapContainerID, aImageMapID, aMapID) { + this.container = getAccessible(aImageMapContainerID); + this.containerNode = this.container.DOMNode; + this.imageMap = null; + this.imgNode = getNode(aImageMapID); + this.mapNode = getNode(aMapID); + + function getImageMap(aThisObj) { + return aThisObj.imageMap; + } + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getImageMap, this), + new invokerChecker(EVENT_SHOW, this.imgNode), + new invokerChecker(EVENT_REORDER, this.container), + ]; + + this.invoke = function removeMap_invoke() { + this.imageMap = getAccessible(aImageMapID); + this.mapNode.remove(); + }; + + this.finalCheck = function removeMap_finalCheck() { + var accTree = + { SECTION: [ + { GRAPHIC: [ ] }, + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function removeMap_getID() { + return "remove map element"; + }; + } + + function insertMap(aImageMapContainerID, aImageID) { + this.container = getAccessible(aImageMapContainerID); + this.containerNode = this.container.DOMNode; + this.image = null; + this.imgMapNode = getNode(aImageID); + + function getImage(aThisObj) { + return aThisObj.image; + } + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getImage, this), + new invokerChecker(EVENT_SHOW, this.imgMapNode), + new invokerChecker(EVENT_REORDER, this.container), + ]; + + this.invoke = function insertMap_invoke() { + this.image = getAccessible(aImageID); + + var map = document.createElement("map"); + map.setAttribute("name", "atoz_map"); + map.setAttribute("id", "map"); + + var area = document.createElement("area"); + 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); + + this.containerNode.appendChild(map); + }; + + this.finalCheck = function insertMap_finalCheck() { + var accTree = + { SECTION: [ + { IMAGE_MAP: [ + { LINK: [ ] }, + ] }, + ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function insertMap_getID() { + return "insert map element"; + }; + } + + function hideImageMap(aContainerID, aImageID) { + this.container = getAccessible(aContainerID); + this.imageMap = null; + this.imageMapNode = getNode(aImageID); + + function getImageMap(aThisObj) { + return aThisObj.imageMap; + } + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getImageMap, this), + new invokerChecker(EVENT_REORDER, aContainerID), + ]; + + this.invoke = function hideImageMap_invoke() { + this.imageMap = getAccessible(this.imageMapNode); + this.imageMapNode.style.display = "none"; + }; + + this.finalCheck = function hideImageMap_finalCheck() { + var accTree = + { SECTION: [ ] }; + testAccessibleTree(this.container, accTree); + }; + + this.getID = function hideImageMap_getID() { + return "display:none image"; + }; + } + + // gA11yEventDumpToConsole = true; // debug stuff + function doPreTest() { + waitForImageMap("imgmap", doTest); + } + + var gQueue = null; + function doTest() { + gQueue = new eventQueue(); + + gQueue.push(new insertArea("imgmap", "map")); + gQueue.push(new appendArea("imgmap", "map")); + gQueue.push(new removeArea("imgmap", "map")); + gQueue.push(new removeNameOnMap("container", "imgmap", "map")); + gQueue.push(new restoreNameOnMap("container", "imgmap", "map")); + gQueue.push(new removeMap("container", "imgmap", "map")); + gQueue.push(new insertMap("container", "imgmap")); + gQueue.push(new hideImageMap("container", "imgmap")); + + // enableLogging("tree"); // debug stuff + // gQueue.onFinish = function() { disableLogging("tree"); } + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doPreTest); + </script> + +</head> +<body> + + <a target="_blank" + title="Image map accessible tree is not updated when image map is changed" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=732389"> + Mozilla Bug 732389 + </a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <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="../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/mochitest/treeupdate/test_list.html b/accessible/tests/mochitest/treeupdate/test_list.html new file mode 100644 index 0000000000..06c308e422 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_list.html @@ -0,0 +1,139 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Test HTML li and listitem bullet accessible cache</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Helpers + + function testLiAccessibleTree() { + // Test accessible tree. + var accTree = { + role: ROLE_LISTITEM, + children: [ + { + role: ROLE_LISTITEM_MARKER, + children: [], + }, + { + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }; + + testAccessibleTree("li", accTree); + } + + // ////////////////////////////////////////////////////////////////////////// + // Sequence item processors + + function hideProcessor() { + this.liNode = getNode("li"); + this.li = getAccessible(this.liNode); + this.bullet = this.li.firstChild; + + this.process = function hideProcessor_process() { + this.liNode.style.display = "none"; + }; + + this.onProcessed = function hideProcessor_onProcessed() { + window.setTimeout( + function(aLiAcc, aLiNode, aBulletAcc) { + testDefunctAccessible(aLiAcc, aLiNode); + testDefunctAccessible(aBulletAcc); + + gSequence.processNext(); + }, + 0, this.li, this.liNode, this.bullet + ); + }; + } + + function showProcessor() { + this.liNode = getNode("li"); + + this.process = function showProcessor_process() { + this.liNode.style.display = "list-item"; + }; + + this.onProcessed = function showProcessor_onProcessed() { + testLiAccessibleTree(); + gSequence.processNext(); + }; + } + + function textReplaceProcessor() { + this.liNode = getNode("li"); + + this.process = function textReplaceProcessor_process() { + this.liNode.textContent = "hey"; + }; + + this.onProcessed = function textReplaceProcessor_onProcessed() { + var tree = { + LISTITEM: [ + { LISTITEM_MARKER: [] }, + { TEXT_LEAF: [] }, + ], + }; + testAccessibleTree(this.liNode, tree); + SimpleTest.finish(); + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpToConsole = true; + + var gSequence = null; + function doTest() { + testLiAccessibleTree(); + + gSequence = new sequence(); + + gSequence.append(new hideProcessor(), EVENT_HIDE, getAccessible("li"), + "hide HTML li"); + gSequence.append(new showProcessor(), EVENT_SHOW, getNode("li"), + "show HTML li"); + gSequence.append(new textReplaceProcessor(), EVENT_REORDER, getNode("li"), + "change text of HTML li"); + + gSequence.processNext(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="setParent shouldn't be virtual" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=496783">Mozilla Bug 496783</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <ul> + <li id="li">item1</li> + </ul> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_list_editabledoc.html b/accessible/tests/mochitest/treeupdate/test_list_editabledoc.html new file mode 100644 index 0000000000..59b9dc9c53 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_list_editabledoc.html @@ -0,0 +1,100 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Test HTML li and listitem bullet accessible insertion into editable document</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function addLi(aID) { + this.listNode = getNode(aID); + this.liNode = document.createElement("li"); + this.liNode.textContent = "item"; + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getAccessible, this.liNode), + new invokerChecker(EVENT_REORDER, this.listNode), + ]; + + this.invoke = function addLi_invoke() { + this.listNode.appendChild(this.liNode); + }; + + this.finalCheck = function addLi_finalCheck() { + var tree = { + role: ROLE_LIST, + children: [ + { + role: ROLE_LISTITEM, + children: [ + { + role: ROLE_LISTITEM_MARKER, + name: "1. ", + children: [], + }, + { + role: ROLE_TEXT_LEAF, + children: [], + }, + ], + }, + ], + }; + testAccessibleTree(aID, tree); + }; + + this.getID = function addLi_getID() { + return "add li"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "eventdump"; // debug stuff + + var gQueue = null; + + function doTest() { + gQueue = new eventQueue(); + + gQueue.push(new addLi("list")); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body contentEditable="true"> + + <a target="_blank" + title="Wrong list bullet text of accessible for the first numbered HTML:li in CKEditor" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=557795">Mozilla Bug 557795</a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <ol id="list"> + </ol> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_listbox.xhtml b/accessible/tests/mochitest/treeupdate/test_listbox.xhtml new file mode 100644 index 0000000000..629c4b0915 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_listbox.xhtml @@ -0,0 +1,181 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL listbox hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Test + + function insertListitem(aListboxID) + { + this.listboxNode = getNode(aListboxID); + + this.listitemNode = document.createXULElement("richlistitem"); + var label = document.createXULElement("label"); + label.setAttribute("value", "item1"); + this.listitemNode.appendChild(label); + + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, this.listitemNode), + new invokerChecker(EVENT_REORDER, this.listboxNode) + ]; + + this.invoke = function insertListitem_invoke() + { + this.listboxNode.insertBefore(this.listitemNode, + this.listboxNode.firstChild); + } + + this.finalCheck = function insertListitem_finalCheck() + { + var tree = + { LISTBOX: [ + { + role: ROLE_RICH_OPTION, + name: "item1" + }, + { + role: ROLE_RICH_OPTION, + name: "item2" + }, + { + role: ROLE_RICH_OPTION, + name: "item3" + }, + { + role: ROLE_RICH_OPTION, + name: "item4" + } + ] }; + testAccessibleTree(this.listboxNode, tree); + } + + this.getID = function insertListitem_getID() + { + return "insert listitem "; + } + } + + function removeListitem(aListboxID) + { + this.listboxNode = getNode(aListboxID); + this.listitemNode = null; + this.listitem; + + function getListitem(aThisObj) + { + return aThisObj.listitem; + } + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getListitem, this), + new invokerChecker(EVENT_REORDER, this.listboxNode) + ]; + + this.invoke = function removeListitem_invoke() + { + this.listitemNode = this.listboxNode.firstChild; + this.listitem = getAccessible(this.listitemNode); + + this.listboxNode.removeChild(this.listitemNode); + } + + this.finalCheck = function removeListitem_finalCheck() + { + var tree = + { LISTBOX: [ + { + role: ROLE_RICH_OPTION, + name: "item2" + }, + { + role: ROLE_RICH_OPTION, + name: "item3" + }, + { + role: ROLE_RICH_OPTION, + name: "item4" + } + ] }; + testAccessibleTree(this.listboxNode, tree); + } + + this.getID = function removeListitem_getID() + { + return "remove listitem "; + } + } + + //gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + function doTest() + { + var tree = + { LISTBOX: [ + { + role: ROLE_RICH_OPTION, + name: "item2" + }, + { + role: ROLE_RICH_OPTION, + name: "item3" + }, + { + role: ROLE_RICH_OPTION, + name: "item4" + } + ] }; + testAccessibleTree("listbox", tree); + + gQueue = new eventQueue(); + gQueue.push(new insertListitem("listbox")); + gQueue.push(new removeListitem("listbox")); + gQueue.invoke(); // Will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=656225" + title="XUL listbox accessible tree doesn't get updated"> + Mozilla Bug 656225 + </a> + <br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <richlistbox id="listbox"> + <richlistitem><label value="item2"/></richlistitem> + <richlistitem><label value="item3"/></richlistitem> + <richlistitem><label value="item4"/></richlistitem> + </richlistbox> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/treeupdate/test_menu.xhtml b/accessible/tests/mochitest/treeupdate/test_menu.xhtml new file mode 100644 index 0000000000..44042cc9e7 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_menu.xhtml @@ -0,0 +1,127 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL menu hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function openMenu(aID) + { + this.menuNode = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_FOCUS, this.menuNode) + ]; + + this.invoke = function openMenu_invoke() + { + var tree; + if (LINUX || SOLARIS) { + tree = + { PARENT_MENUITEM: [ ] }; + + } else { + tree = + { PARENT_MENUITEM: [ + { MENUPOPUP: [ ] } + ] }; + } + testAccessibleTree(aID, tree); + + // Show menu. + this.menuNode.open = true; + } + + this.finalCheck = function openMenu_finalCheck() + { + var tree; + if (LINUX || SOLARIS) { + tree = + { PARENT_MENUITEM: [ + { MENUITEM: [ ] }, + { MENUITEM: [ ] } + ] }; + + } else { + tree = + { PARENT_MENUITEM: [ + { MENUPOPUP: [ + { MENUITEM: [ ] }, + { MENUITEM: [ ] } + ] } + ] }; + } + testAccessibleTree(aID, tree); + } + + this.getID = function openMenu_getID() + { + return "open menu " + prettyName(aID); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + var gQueue = null; + function doTest() + { + gQueue = new eventQueue(); + gQueue.push(new openMenu("menu")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=249292" + title="Ensure accessible children for toolbarbutton types 'menu'"> + Mozilla Bug 249292 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=630486" + title="Don't force accessible creation for popup children."> + Mozilla Bug 630486 + </a> + <br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <menubar> + <menu id="menu" label="menu"> + <menupopup> + <menuitem label="menuitem"/> + <menuitem label="menuitem"/> + </menupopup> + </menu> + </menubar> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/treeupdate/test_menubutton.xhtml b/accessible/tests/mochitest/treeupdate/test_menubutton.xhtml new file mode 100644 index 0000000000..4684337b3d --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_menubutton.xhtml @@ -0,0 +1,140 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL button hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + + //////////////////////////////////////////////////////////////////////////// + // Invokers + + function openMenu(aButtonID, aMenuItemRole) + { + var menuItemRole = aMenuItemRole || ROLE_MENUITEM; + this.button = getAccessible(aButtonID); + this.menupopup = this.button.firstChild; + + var checker = new invokerChecker(EVENT_REORDER, this.menupopup); + this.__proto__ = new synthClick(aButtonID, checker); + + this.invoke = function openMenu_invoke() + { + var tree = + { PUSHBUTTON: [ + { MENUPOPUP: [ ] } + ] }; + testAccessibleTree(this.button, tree); + + this.__proto__.invoke(); + } + + this.finalCheck = function openMenu_finalCheck() + { + var tree = + { PUSHBUTTON: [ + { MENUPOPUP: [ + { role: menuItemRole, children: [ ] }, + { role: menuItemRole, children: [ ] } + ] } + ] }; + testAccessibleTree(this.button, tree); + + synthesizeKey("KEY_Escape"); + } + + this.getID = function openMenu_getID() + { + return "open menu of the button " + prettyName(aButtonID); + } + } + + //////////////////////////////////////////////////////////////////////////// + // Do test + + gA11yEventDumpToConsole = true; // debug stuff + + var gQueue = null; + + function doTest() + { + gQueue = new eventQueue(); + + gQueue.push(new openMenu("button1")); + gQueue.push(new openMenu("button3")); + + var columnPickerBtn = getAccessible("tree").firstChild.lastChild; + gQueue.push(new openMenu(columnPickerBtn, ROLE_CHECK_MENU_ITEM)); + gQueue.invoke(); // SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=249292" + title="Ensure accessible children for toolbarbutton types 'menu'"> + Bug 249292 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=630486" + title="Don't force accessible creation for popup children"> + Bug 630486 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=722265" + title="Column header selection popup no longer exposed to accessibility APIs"> + Bug 722265 + </a> + <br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <button id="button1" type="menu" label="button"> + <menupopup> + <menuitem label="menuitem"/> + <menuitem label="menuitem"/> + </menupopup> + </button> + + <toolbarbutton id="button3" type="menu" label="toolbarbutton"> + <menupopup> + <menuitem label="menuitem"/> + <menuitem label="menuitem"/> + </menupopup> + </toolbarbutton> + + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="another column"/> + </treecols> + <treechildren/> + </tree> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/treeupdate/test_optgroup.html b/accessible/tests/mochitest/treeupdate/test_optgroup.html new file mode 100644 index 0000000000..288de1dd1d --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_optgroup.html @@ -0,0 +1,126 @@ +<!DOCTYPE html> +<html> +<head> + <title>Add and remove optgroup test</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + function addOptGroup(aID) { + this.selectNode = getNode(aID); + this.select = getAccessible(this.selectNode); + this.selectList = this.select.firstChild; + + this.invoke = function addOptGroup_invoke() { + var optGroup = document.createElement("optgroup"); + for (let i = 0; i < 2; i++) { + var opt = document.createElement("option"); + opt.value = i; + opt.text = "Option: Value " + i; + + optGroup.appendChild(opt); + } + + this.selectNode.add(optGroup, null); + var option = document.createElement("option"); + this.selectNode.add(option, null); + }; + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.selectList), + ]; + + this.finalCheck = function addOptGroup_finalCheck() { + var tree = + { COMBOBOX: [ + { COMBOBOX_LIST: [ + { GROUPING: [ + { COMBOBOX_OPTION: [ + { TEXT_LEAF: [] }, + ] }, + { COMBOBOX_OPTION: [ + { TEXT_LEAF: [] }, + ] }, + ]}, + { COMBOBOX_OPTION: [] }, + ] }, + ] }; + testAccessibleTree(this.select, tree); + }; + + this.getID = function addOptGroup_getID() { + return "test optgroup's insertion into a select"; + }; + } + + function removeOptGroup(aID) { + this.selectNode = getNode(aID); + this.select = getAccessible(this.selectNode); + this.selectList = this.select.firstChild; + + this.invoke = function removeOptGroup_invoke() { + this.option1Node = this.selectNode.firstChild.firstChild; + this.selectNode.firstChild.remove(); + }; + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.selectList), + ]; + + this.finalCheck = function removeOptGroup_finalCheck() { + var tree = + { COMBOBOX: [ + { COMBOBOX_LIST: [ + { COMBOBOX_OPTION: [] }, + ] }, + ] }; + testAccessibleTree(this.select, tree); + is(isAccessible(this.option1Node), false, "removed option shouldn't be accessible anymore!"); + }; + + this.getID = function removeOptGroup_getID() { + return "test optgroup's removal from a select"; + }; + } + + // gA11yEventDumpToConsole = true; + + function doTest() { + const gQueue = new eventQueue(); + + gQueue.push(new addOptGroup("select")); + gQueue.push(new removeOptGroup("select")); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=616452" + title="Bug 616452 - Dynamically inserted select options aren't reflected in accessible tree"> + Bug 616452</a> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="select"></select> + + <div id="debug"/> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_recreation.html b/accessible/tests/mochitest/treeupdate/test_recreation.html new file mode 100644 index 0000000000..92cf892569 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_recreation.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Test accessible recreation</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + + async function doTest() { + let events, msg; + + msg = "Assign a 'button' role to a span"; + events = waitForOrderedEvents( + [[EVENT_HIDE], [EVENT_SHOW, "span"], [EVENT_REORDER, "container"]], msg); + document.getElementById("span").setAttribute("role", "button"); + await events; + + msg = "Remove the 'button' role from a span"; + events = waitForOrderedEvents( + [[EVENT_HIDE, "span"], [EVENT_SHOW], [EVENT_REORDER, "container"]], msg); + document.getElementById("span").removeAttribute("role"); + await events; + + msg = "Assign a 'button' role to a div"; + events = waitForOrderedEvents( + [[EVENT_HIDE, "div1"], [EVENT_SHOW, "div1"], [EVENT_REORDER, "container"]], msg); + document.getElementById("div1").setAttribute("role", "button"); + await events; + + msg = "Set a listbox as multiselectable"; + events = waitForOrderedEvents( + [[EVENT_HIDE, "div3"], [EVENT_SHOW, "div3"], [EVENT_REORDER, "container"]], msg); + document.getElementById("div3").setAttribute("aria-multiselectable", "true"); + await events; + + msg = "Change a password field to a text field"; + events = waitForOrderedEvents( + [[EVENT_HIDE, "password"], [EVENT_SHOW, "password"], [EVENT_REORDER, "container"]], msg); + document.getElementById("password").setAttribute("type", "text"); + await events; + + msg = "Change a text field to a password field"; + events = waitForOrderedEvents( + [[EVENT_HIDE, "text"], [EVENT_SHOW, "text"], [EVENT_REORDER, "container"]], msg); + document.getElementById("text").setAttribute("type", "password"); + await events; + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Rework accessible tree update code" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=570275"> + Mozilla Bug 570275 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="container"> + <span id="span">span</span> + <div id="div1">div</div> + <a id="anchor">anchor</a> + <div id="div3" role="listbox">list</div> + <input type="password" id="password"/> + <input type="text" id="text"/> + </div> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_select.html b/accessible/tests/mochitest/treeupdate/test_select.html new file mode 100644 index 0000000000..c4c8c8b8f6 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_select.html @@ -0,0 +1,136 @@ +<!DOCTYPE html> +<html> +<head> + <title>HTML select options test</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + function addOptions(aID) { + this.selectNode = getNode(aID); + this.select = getAccessible(this.selectNode); + this.selectList = this.select.firstChild; + + this.invoke = function addOptions_invoke() { + for (let i = 0; i < 2; i++) { + var opt = document.createElement("option"); + opt.value = i; + opt.text = "Option: Value " + i; + + this.selectNode.add(opt, null); + } + }; + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.selectList), + ]; + + this.finalCheck = function addOptions_finalCheck() { + var tree = + { COMBOBOX: [ + { COMBOBOX_LIST: [ + { COMBOBOX_OPTION: [ + { TEXT_LEAF: [] }, + ] }, + { COMBOBOX_OPTION: [ + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }; + testAccessibleTree(this.select, tree); + }; + + this.getID = function addOptions_getID() { + return "test elements insertion into a select"; + }; + } + + function removeOptions(aID) { + this.selectNode = getNode(aID); + this.select = getAccessible(this.selectNode); + this.selectList = this.select.firstChild; + + this.invoke = function removeOptions_invoke() { + while (this.selectNode.length) + this.selectNode.remove(0); + }; + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.selectList), + ]; + + this.finalCheck = function removeOptions_finalCheck() { + var tree = + { COMBOBOX: [ + { COMBOBOX_LIST: [] }, + ] }; + testAccessibleTree(this.select, tree); + }; + + this.getID = function removeptions_getID() { + return "test elements removal from a select"; + }; + } + + /** + * Setting role=option on option makes the accessible recreate. + */ + function setRoleOnOption() { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, "s2_o"), + new invokerChecker(EVENT_SHOW, "s2_o"), + ]; + + this.invoke = function setRoleOnOption_setRole() { + getNode("s2_o").setAttribute("role", "option"); + }; + + this.finalCheck = function() { + var tree = + { COMBOBOX: [ + { COMBOBOX_LIST: [ + { COMBOBOX_OPTION: [ ] }, + ] }, + ] }; + testAccessibleTree("s2", tree); + }; + + this.getID = function removeptions_getID() { + return "setting role=option on select option"; + }; + } + + function doTest() { + const gQueue = new eventQueue(); + + gQueue.push(new addOptions("select")); + gQueue.push(new removeOptions("select")); + gQueue.push(new setRoleOnOption()); + + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <select id="select"></select> + <select id="s2"><option id="s2_o"></option></select> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_shadow_slots.html b/accessible/tests/mochitest/treeupdate/test_shadow_slots.html new file mode 100644 index 0000000000..1a42fc4d52 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_shadow_slots.html @@ -0,0 +1,467 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Test shadow roots with slots</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../promisified-events.js"></script> + + <script type="application/javascript"> + async function _dynamicShadowTest(name, mutationFunc, expectedTree, reorder_targets) { + info(name); + + let container = getNode(name); + let host = container.querySelector('.host'); + + document.body.offsetTop; + let event = reorder_targets ? + waitForEvents(reorder_targets.map(target => [EVENT_REORDER, target, name])) : + waitForEvent(EVENT_REORDER, host, name); + + mutationFunc(container, host); + + await event; + + testAccessibleTree(container, expectedTree); + + return true; + } + + async function attachFlatShadow() { + await _dynamicShadowTest("attachFlatShadow", + (container, host) => { + host.attachShadow({ mode: "open" }) + .appendChild(container.querySelector('.shadowtree').content.cloneNode(true)); + }, { SECTION: [{ SECTION: [{ name: "red"} ] }] }); + } + + async function attachOneDeepShadow() { + await _dynamicShadowTest("attachOneDeepShadow", + (container, host) => { + host.attachShadow({ mode: "open" }) + .appendChild(container.querySelector('.shadowtree').content.cloneNode(true)); + }, { SECTION: [{ SECTION: [{ SECTION: [{ name: "red"} ] }] }] }); + } + + async function changeSlotFlat() { + await _dynamicShadowTest("changeSlotFlat", + (container, host) => { + container.querySelector('.red').removeAttribute('slot'); + container.querySelector('.green').setAttribute('slot', 'myslot'); + }, { SECTION: [{ SECTION: [{ name: "green"} ] }] }); + } + + async function changeSlotOneDeep() { + await _dynamicShadowTest("changeSlotOneDeep", + (container, host) => { + container.querySelector('.red').removeAttribute('slot'); + container.querySelector('.green').setAttribute('slot', 'myslot'); + }, { SECTION: [{ SECTION: [{ SECTION: [{ name: "green"} ] }] }] }, ["shadowdiv"]); + } + + // Nested roots and slots + async function changeSlotNested() { + await _dynamicShadowTest("changeSlotNested", + (container, host) => { + testAccessibleTree(getNode("changeSlotNested"), + { SECTION: [{ SECTION: [{ SECTION: [{ name: "red"} ] }] }] }); + container.querySelector('.red').removeAttribute('slot'); + container.querySelector('.green').setAttribute('slot', 'myslot'); + }, { SECTION: [{ SECTION: [{ SECTION: [{ name: "green"} ] }] }] }, ["shadowdiv"]); + } + + // Dynamic mutations to both shadow root and shadow host subtrees + // testing/web-platform/tests/css/css-scoping/shadow-assign-dynamic-001.html + async function assignSlotDynamic() { + await _dynamicShadowTest("assignSlotDynamic", + (container, host) => { + host.shadowRoot.appendChild(container.querySelector('.shadowtree').content.cloneNode(true)); + host.appendChild(container.querySelector('.lighttree').content.cloneNode(true)); + }, { SECTION: [{ SECTION: [{ name: "slot1"}, { name: "slot2" } ] }] }); + } + + // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-001.html + async function shadowFallbackDynamic_1() { + await _dynamicShadowTest("shadowFallbackDynamic_1", + (container, host) => { + host.firstElementChild.remove(); + }, { SECTION: [{ SECTION: [{ name: "green"} ] }] }); + } + + // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-002.html + async function shadowFallbackDynamic_2() { + await _dynamicShadowTest("shadowFallbackDynamic_2", + (container, host) => { + host.firstElementChild.removeAttribute("slot"); + }, { SECTION: [{ SECTION: [{ name: "green"} ] }] }); + } + + // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-003.html + async function shadowFallbackDynamic_3() { + await _dynamicShadowTest("shadowFallbackDynamic_3", + (container, host) => { + host.appendChild(container.querySelector(".lighttree").content.cloneNode(true)); + }, { SECTION: [{ SECTION: [{ name: "green"} ] }] }); + } + + // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-004.html + async function shadowFallbackDynamic_4() { + await _dynamicShadowTest("shadowFallbackDynamic_4", + (container, host) => { + host.shadowRoot.insertBefore( + container.querySelector(".moreshadowtree"). + content.cloneNode(true), host.shadowRoot.firstChild); + }, { SECTION: [{ SECTION: [{ name: "slotparent2", children: [{ name: "green"} ] }, { name: "slotparent1" } ] }] }); + } + + // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-004.html + // This tests a case when the the slotted element would remain in the same accessible container + async function shadowFallbackDynamic_4_1() { + await _dynamicShadowTest("shadowFallbackDynamic_4_1", + (container, host) => { + host.shadowRoot.insertBefore( + container.querySelector(".moreshadowtree"). + content.cloneNode(true), host.shadowRoot.firstChild); + }, { SECTION: [{ SECTION: [ { name: "green"}, { SEPARATOR: [] } ] }] }); + } + + // testing/web-platform/tests/css/css-scoping/shadow-fallback-dynamic-005.html + async function shadowFallbackDynamic_5() { + await _dynamicShadowTest("shadowFallbackDynamic_5", + (container, host) => { + host.firstElementChild.setAttribute("slot", "myotherslot"); + }, { SECTION: [{ SECTION: [{ name: "green"} ] }] }); + } + + // testing/web-platform/tests/css/css-scoping/shadow-reassign-dynamic-002.html + async function shadowReassignDynamic_2() { + await _dynamicShadowTest("shadowReassignDynamic_2", + (container, host) => { + host.shadowRoot.querySelector("slot").setAttribute("name", "myslot"); + }, { SECTION: [{ SECTION: [{ name: "green"} ] }] }); + } + + // testing/web-platform/tests/css/css-scoping/shadow-reassign-dynamic-003.html + async function shadowReassignDynamic_3() { + await _dynamicShadowTest("shadowReassignDynamic_3", + (container, host) => { + testAccessibleTree(container, { SECTION: [{ SECTION: [{ name: "green"}, { name: "red", children: [ { PUSHBUTTON: [] }]} ] }] }); + host.shadowRoot.querySelector("slot[name]").removeAttribute("name"); + + }, { SECTION: [{ SECTION: [{ name: "green", children: [ { PUSHBUTTON: [] }]}, { name: "red"} ] }] }, + [evt => evt.accessible.name == "green", evt => evt.accessible.name == "red"]); + } + + // testing/web-platform/tests/css/css-scoping/shadow-reassign-dynamic-004.html + async function shadowReassignDynamic_4() { + await _dynamicShadowTest("shadowReassignDynamic_4", + (container, host) => { + host.shadowRoot.getElementById("slot").remove(); + }, { SECTION: [{ SECTION: [{ name: "green"} ] }] }); + + } + + async function doTest() { + await attachFlatShadow(); + + await attachOneDeepShadow(); + + await changeSlotFlat(); + + await changeSlotOneDeep(); + + await changeSlotNested(); + + await assignSlotDynamic(); + + await shadowFallbackDynamic_1(); + + await shadowFallbackDynamic_2(); + + await shadowFallbackDynamic_3(); + + await shadowFallbackDynamic_4(); + + await shadowFallbackDynamic_4_1(); + + await shadowFallbackDynamic_5(); + + await shadowReassignDynamic_2(); + + await shadowReassignDynamic_3(); + + await shadowReassignDynamic_4(); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + <div id="attachFlatShadow"> + <template class="shadowtree"> + <style>::slotted(div) { width: 100px; height: 100px }</style> + <slot name="myslot">FAIL</slot> + </template> + <section class="host"> + <div style="background: green" aria-label="green"></div> + <div style="background: red" aria-label="red" slot="myslot"></div> + </section> + </div> + + <div id="attachOneDeepShadow"> + <template class="shadowtree"> + <style>::slotted(div) { width: 100px; height: 100px }</style> + <div id="shadowdiv"> + <slot name="myslot">FAIL</slot> + </div> + </template> + <section class="host"> + <div style="background: green" aria-label="green"></div> + <div style="background: red" aria-label="red" slot="myslot"></div> + </section> + </div> + + <div id="changeSlotFlat"> + <template class="shadowtree"> + <style>::slotted(div) { width: 100px; height: 100px }</style> + <slot name="myslot">FAIL</slot> + </template> + <section class="host"> + <div class="green" style="background: green" aria-label="green"></div> + <div class="red" style="background: red" aria-label="red" slot="myslot"></div> + </section> + <script> + document.querySelector("#changeSlotFlat > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#changeSlotFlat > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="changeSlotOneDeep"> + <template class="shadowtree"> + <style>::slotted(div) { width: 100px; height: 100px }</style> + <div id="shadowdiv"> + <slot name="myslot">FAIL</slot> + </div> + </template> + <section class="host"> + <div class="green" style="background: green" aria-label="green"></div> + <div class="red" style="background: red" aria-label="red" slot="myslot"></div> + </section> + <script> + document.querySelector("#changeSlotOneDeep > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#changeSlotOneDeep > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="changeSlotNested"> + <template class="shadowtree outer"> + <div id="shadowdiv"> + <slot name="myslot">FAIL</slot> + </div> + </template> + <template class="shadowtree inner"> + <style>::slotted(div) { width: 100px; height: 100px }</style> + <slot>FAIL</slot> + </template> + <section class="host"> + <div class="green" style="background: green" aria-label="green"></div> + <div class="red" style="background: red" aria-label="red" slot="myslot"></div> + </section> + <script> + (function foo() { + let outerShadow = + document.querySelector("#changeSlotNested > .host"). + attachShadow({ mode: "open" }); + outerShadow.appendChild( + document.querySelector("#changeSlotNested > .shadowtree.outer"). + content.cloneNode(true)); + let innerShadow = + outerShadow.querySelector("#shadowdiv"). + attachShadow({ mode: "open" }); + innerShadow.appendChild( + document.querySelector("#changeSlotNested > .shadowtree.inner"). + content.cloneNode(true)); + })(); + </script> + </div> + + <div id="assignSlotDynamic"> + <template class="shadowtree"> + <style>::slotted(div) { width: 50px; height: 100px }</style> + <slot name="slot1">FAIL</slot> + <slot name="slot2">FAIL</slot> + </template> + <template class="lighttree"> + <div aria-label="slot1" slot="slot1"></div> + <div aria-label="slot2" slot="slot2"></div> + </template> + <section class="host"></section> + <script> + document.querySelector("#assignSlotDynamic > .host").attachShadow({ mode: "open" }); + </script> + </div> + + <div id="shadowFallbackDynamic_1"> + <template class="shadowtree"> + <slot name="myslot"> + <div aria-label="green" style="width: 100px; height: 100px; background: green"></div> + </slot> + </template> + <section class="host"><span slot="myslot">FAIL</span></section> + <script> + document.querySelector("#shadowFallbackDynamic_1 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowFallbackDynamic_1 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="shadowFallbackDynamic_2"> + <template class="shadowtree"> + <slot name="myslot"> + <div aria-label="green" style="width: 100px; height: 100px; background: green"></div> + </slot> + </template> + <section class="host"><span slot="myslot">FAIL</span></section> + <script> + document.querySelector("#shadowFallbackDynamic_2 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowFallbackDynamic_2 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="shadowFallbackDynamic_3"> + <template class="shadowtree"> + <slot name="myslot">FAIL</slot> + </template> + <template class="lighttree"> + <div aria-label="green" slot="myslot" style="width: 100px; height: 100px; background: green"></div> + </template> + <section class="host"></section> + <script> + document.querySelector("#shadowFallbackDynamic_3 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowFallbackDynamic_3 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="shadowFallbackDynamic_4"> + <template class="shadowtree"> + <div aria-label="slotparent1"><slot name="myslot"></slot></div> + </template> + <template class="moreshadowtree"> + <div aria-label="slotparent2"><slot name="myslot">FAIL</slot></div> + </template> + <section class="host"> + <div slot="myslot" aria-label="green" style="width: 100px; height: 100px; background: green"></div> + </section> + <script> + document.querySelector("#shadowFallbackDynamic_4 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowFallbackDynamic_4 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="shadowFallbackDynamic_4_1"> + <template class="shadowtree"> + <hr> + <slot name="myslot"></slot> + </template> + <template class="moreshadowtree"> + <slot name="myslot">FAIL</slot> + </template> + <section class="host"> + <div slot="myslot" aria-label="green" style="width: 100px; height: 100px; background: green"></div> + </section> + <script> + document.querySelector("#shadowFallbackDynamic_4_1 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowFallbackDynamic_4_1 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="shadowFallbackDynamic_5"> + <template class="shadowtree"> + <slot name="myslot"></slot> + <slot name="myotherslot">FAIL</slot> + </template> + <section class="host"> + <div slot="myslot" aria-label="green" style="width: 100px; height: 100px; background: green"></div> + </section> + <script> + document.querySelector("#shadowFallbackDynamic_5 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowFallbackDynamic_5 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="shadowReassignDynamic_2"> + <template class="shadowtree"> + <style>::slotted(div) { width: 100px; height: 100px }</style> + <slot>FAIL</slot> + </template> + <section class="host"> + <div slot="myslot" aria-label="green" style="width: 100px; height: 100px; background: green"></div> + </section> + <script> + document.querySelector("#shadowReassignDynamic_2 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowReassignDynamic_2 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="shadowReassignDynamic_3"> + <template class="shadowtree"> + <div aria-label="green"><slot name="nomatch"></slot></div> + <div aria-label="red"><slot></slot></div> + </template> + <section class="host"> + <div role="button"></div> + </section> + <script> + document.querySelector("#shadowReassignDynamic_3 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowReassignDynamic_3 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="shadowReassignDynamic_4"> + <template class="shadowtree"> + <style>::slotted(div),div { width: 100px; height: 100px }</style> + <slot id="slot"></slot> + <slot> + <div aria-label="red" style="background: red"></div> + </slot> + </template> + <section class="host"> + <div aria-label="green" style="background: green"></div> + </section> + <script> + document.querySelector("#shadowReassignDynamic_4 > .host") + .attachShadow({ mode: "open" }) + .appendChild(document.querySelector("#shadowReassignDynamic_4 > .shadowtree").content.cloneNode(true)); + </script> + </div> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_shutdown.xhtml b/accessible/tests/mochitest/treeupdate/test_shutdown.xhtml new file mode 100644 index 0000000000..ad8aebf812 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_shutdown.xhtml @@ -0,0 +1,131 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tree hierarchy tests"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + + <script type="application/javascript" + src="../treeview.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../states.js" /> + <script type="application/javascript" + src="../events.js" /> + + <script type="application/javascript"> + <![CDATA[ + function setXULTreeView(aTreeID, aTreeView) + { + this.treeNode = getNode(aTreeID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.treeNode) + ]; + + this.invoke = function loadXULTree_invoke() + { + this.treeNode.view = aTreeView; + }; + + this.getID = function loadXULTree_getID() + { + return "Load XUL tree " + prettyName(aTreeID); + }; + } + + function removeTree(aID) + { + this.tree = getAccessible(aID); + this.lastItem = null; + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, document) + ]; + + this.invoke = function invoke() + { + this.lastItem = getAccessible(aID).lastChild; + this.lastCell = this.lastItem.lastChild; + getNode(aID).remove(); + }; + + this.check = function check(aEvent) + { + testIsDefunct(this.tree, aID); + testIsDefunct(this.lastItem, "last item of " + aID); + if (this.lastCell) { + testIsDefunct(this.lastCell, "last item cell of " + aID); + } + }; + + this.getID = function getID() + { + return "Remove tree from DOM"; + }; + } + + //////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "debug"; + var gQueue = null; + + function doTest() + { + gQueue = new eventQueue(); + + gQueue.push(new setXULTreeView("tree", new nsTreeTreeView())); + gQueue.push(new removeTree("tree")); + + gQueue.push(new setXULTreeView("treetable", new nsTreeTreeView())); + gQueue.push(new removeTree("treetable")); + + gQueue.invoke(); // Will call SimpleTest.finish() + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=503727" + title="Reorganize implementation of XUL tree accessibility"> + Bug 503727 + </a><br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1"> + <tree id="tree" flex="1"> + <treecols> + <treecol id="col" flex="1" primary="true" label="column"/> + </treecols> + <treechildren/> + </tree> + + <tree id="treetable" flex="1"> + <treecols> + <treecol id="col1" flex="1" primary="true" label="column"/> + <treecol id="col2" flex="1" label="column 2"/> + </treecols> + <treechildren/> + </tree> + </vbox> + </hbox> + +</window> diff --git a/accessible/tests/mochitest/treeupdate/test_table.html b/accessible/tests/mochitest/treeupdate/test_table.html new file mode 100644 index 0000000000..50fac91757 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_table.html @@ -0,0 +1,74 @@ +<!DOCTYPE html> +<html> +<head> + <title>Table update tests</title> + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + function appendCaption(aTableID) { + this.invoke = function appendCaption_invoke() { + // append a caption, it should appear as a first element in the + // accessible tree. + var caption = document.createElement("caption"); + caption.textContent = "table caption"; + getNode(aTableID).appendChild(caption); + }; + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, aTableID), + ]; + + this.finalCheck = function appendCaption_finalCheck() { + var tree = + { TABLE: [ + { CAPTION: [ + { TEXT_LEAF: [] }, + ] }, + { ROW: [ + { CELL: [ {TEXT_LEAF: [] }]}, + { CELL: [ {TEXT_LEAF: [] }]}, + ] }, + ] }; + testAccessibleTree(aTableID, tree); + }; + + this.getID = function appendCaption_getID() { + return "append caption"; + }; + } + + function doTest() { + const gQueue = new eventQueue(); + gQueue.push(new appendCaption("table")); + gQueue.invoke(); // Will call SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <table id="table"> + <tr> + <td>cell1</td> + <td>cell2</td> + </tr> + </table> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_textleaf.html b/accessible/tests/mochitest/treeupdate/test_textleaf.html new file mode 100644 index 0000000000..f0181dd754 --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_textleaf.html @@ -0,0 +1,167 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Test accessible recreation</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + function textLeafUpdate(aID, aIsTextLeafLinkable) { + this.node = getNode(aID); + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.node.parentNode), + ]; + + this.finalCheck = function textLeafUpdate_finalCheck() { + var textLeaf = getAccessible(this.node).firstChild; + is(textLeaf.actionCount, (aIsTextLeafLinkable ? 1 : 0), + "Wrong action numbers!"); + }; + } + + function setOnClickAttr(aID) { + var node = getNode(aID); + node.setAttribute("onclick", "alert(3);"); + var textLeaf = getAccessible(node).firstChild; + is(textLeaf.actionCount, 1, "setOnClickAttr: wrong action numbers!"); + } + + function removeOnClickAttr(aID) { + var node = getNode(aID); + node.removeAttribute("onclick"); + var textLeaf = getAccessible(node).firstChild; + is(textLeaf.actionCount, 0, + "removeOnClickAttr: wrong action numbers!"); + } + + function setOnClickNRoleAttrs(aID) { + this.__proto__ = new textLeafUpdate(aID, true); + + this.invoke = function setOnClickAttr_invoke() { + this.node.setAttribute("role", "link"); + this.node.setAttribute("onclick", "alert(3);"); + }; + + this.getID = function setOnClickAttr_getID() { + return "make " + prettyName(aID) + " linkable"; + }; + } + + function removeTextData(aID, aRole) { + this.containerNode = getNode(aID); + this.textNode = this.containerNode.firstChild; + + this.eventSeq = [ + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.invoke = function removeTextData_invoke() { + var tree = { + role: aRole, + children: [ + { + role: ROLE_TEXT_LEAF, + name: "text", + }, + ], + }; + testAccessibleTree(this.containerNode, tree); + + this.textNode.data = ""; + }; + + this.finalCheck = function removeTextData_finalCheck() { + var tree = { + role: aRole, + children: [], + }; + testAccessibleTree(this.containerNode, tree); + }; + + this.getID = function removeTextData_finalCheck() { + return "remove text data of text node inside '" + aID + "'."; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTest() { + // adds onclick on element, text leaf should inherit its action + setOnClickAttr("div"); + // remove onclick attribute, text leaf shouldn't have any action + removeOnClickAttr("div"); + + // Call rest of event tests. + gQueue = new eventQueue(); + + // set onclick attribute making span accessible, it's inserted into tree + // and adopts text leaf accessible, text leaf should have an action + gQueue.push(new setOnClickNRoleAttrs("span")); + + // text data removal of text node should remove its text accessible + gQueue.push(new removeTextData("p", ROLE_PARAGRAPH)); + gQueue.push(new removeTextData("pre", ROLE_TEXT_CONTAINER)); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Clean up the code of accessible initialization and binding to the tree" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=545465"> + Mozilla Bug 545465 + </a> + <a target="_blank" + title="Make sure accessible tree is correct when rendered text is changed" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=625652"> + Mozilla Bug 625652 + </a> + <a target="_blank" + title="Remove text accesible getting no text inside a preformatted area" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=706335"> + Mozilla Bug 706335 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <div id="container"> + <div id="div">div</div> + <span id="span">span</span> + </div> + + <p id="p">text</p> + <pre id="pre">text</pre> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_tooltip.xhtml b/accessible/tests/mochitest/treeupdate/test_tooltip.xhtml new file mode 100644 index 0000000000..816cfec77c --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_tooltip.xhtml @@ -0,0 +1,71 @@ +<?xml version="1.0"?> +<?xml-stylesheet href="chrome://global/skin" type="text/css"?> +<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" + type="text/css"?> + +<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" + title="Accessible XUL tooltip test"> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js" /> + <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js" /> + + <script type="application/javascript" + src="../common.js" /> + <script type="application/javascript" + src="../role.js" /> + <script type="application/javascript" + src="../promisified-events.js" /> + + <script type="application/javascript"> + <![CDATA[ + + async function doTest() { + let tooltip = document.getElementById("tooltip"); + + testAccessibleTree("tooltip-container", { GROUPING: [] }); + + let shown = waitForEvent(EVENT_SHOW, tooltip); + tooltip.openPopup(); + await shown; + + testAccessibleTree("tooltip-container", + { GROUPING: [ { TOOLTIP: [] }] }); + + let hidden = waitForEvent(EVENT_HIDE, tooltip); + tooltip.hidePopup(); + await hidden; + + testAccessibleTree("tooltip-container", { GROUPING: [] }); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + ]]> + </script> + + <hbox flex="1" style="overflow: auto;"> + <body xmlns="http://www.w3.org/1999/xhtml"> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=1652211" + title="Added anonymous tooltip to mochitest docs messes with text"> + Bug 1652211 + </a> + <br/> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + </body> + + <vbox flex="1" role="group" id="tooltip-container"> + <tooltip id="tooltip"> + <description class="tooltip-label" value="hello world"/> + </tooltip> + </vbox> + </hbox> + +</window> + diff --git a/accessible/tests/mochitest/treeupdate/test_visibility.html b/accessible/tests/mochitest/treeupdate/test_visibility.html new file mode 100644 index 0000000000..4107832b3e --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_visibility.html @@ -0,0 +1,411 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Style visibility tree update test</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + /** + * Hide parent while child stays visible. + */ + function test1(aContainerID, aParentID, aChildID) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode(aParentID)), + new invokerChecker(EVENT_SHOW, getNode(aChildID)), + new invokerChecker(EVENT_REORDER, getNode(aContainerID)), + ]; + + this.invoke = function invoke() { + var tree = + { SECTION: [ + { SECTION: [ + { SECTION: [ + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + + getNode(aParentID).style.visibility = "hidden"; + }; + + this.finalCheck = function finalCheck() { + var tree = + { SECTION: [ + { SECTION: [ + { TEXT_LEAF: [] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + }; + + this.getID = function getID() { + return "hide parent while child stays visible"; + }; + } + + /** + * Hide grand parent while its children stay visible. + */ + function test2(aContainerID, aGrandParentID, aChildID, aChild2ID) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode(aGrandParentID)), + new invokerChecker(EVENT_SHOW, getNode(aChildID)), + new invokerChecker(EVENT_SHOW, getNode(aChild2ID)), + new invokerChecker(EVENT_REORDER, getNode(aContainerID)), + ]; + + this.invoke = function invoke() { + var tree = + { SECTION: [ // container + { SECTION: [ // grand parent + { SECTION: [ + { SECTION: [ // child + { TEXT_LEAF: [] }, + ] }, + { SECTION: [ // child2 + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + + getNode(aGrandParentID).style.visibility = "hidden"; + }; + + this.finalCheck = function finalCheck() { + var tree = + { SECTION: [ // container + { SECTION: [ // child + { TEXT_LEAF: [] }, + ] }, + { SECTION: [ // child2 + { TEXT_LEAF: [] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + }; + + this.getID = function getID() { + return "hide grand parent while its children stay visible"; + }; + } + + /** + * Change container style, hide parents while their children stay visible. + */ + function test3(aContainerID, aParentID, aParent2ID, aChildID, aChild2ID) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode(aParentID)), + new invokerChecker(EVENT_HIDE, getNode(aParent2ID)), + new invokerChecker(EVENT_SHOW, getNode(aChildID)), + new invokerChecker(EVENT_SHOW, getNode(aChild2ID)), + new invokerChecker(EVENT_REORDER, getNode(aContainerID)), + ]; + + this.invoke = function invoke() { + var tree = + { SECTION: [ // container + { SECTION: [ // parent + { SECTION: [ // child + { TEXT_LEAF: [] }, + ] }, + ] }, + { SECTION: [ // parent2 + { SECTION: [ // child2 + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + + getNode(aContainerID).style.color = "red"; + getNode(aParentID).style.visibility = "hidden"; + getNode(aParent2ID).style.visibility = "hidden"; + }; + + this.finalCheck = function finalCheck() { + var tree = + { SECTION: [ // container + { SECTION: [ // child + { TEXT_LEAF: [] }, + ] }, + { SECTION: [ // child2 + { TEXT_LEAF: [] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + }; + + this.getID = function getID() { + return "change container style, hide parents while their children stay visible"; + }; + } + + /** + * Change container style and make visible child inside the table. + */ + function test4(aContainerID, aChildID) { + this.eventSeq = [ + new invokerChecker(EVENT_SHOW, getNode(aChildID)), + new invokerChecker(EVENT_REORDER, getNode(aChildID).parentNode), + ]; + + this.invoke = function invoke() { + var tree = + { SECTION: [ + { TABLE: [ + { ROW: [ + { CELL: [ ] }, + ] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + + getNode(aContainerID).style.color = "red"; + getNode(aChildID).style.visibility = "visible"; + }; + + this.finalCheck = function finalCheck() { + var tree = + { SECTION: [ + { TABLE: [ + { ROW: [ + { CELL: [ + { SECTION: [ + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + }; + + this.getID = function getID() { + return "change container style, make visible child insdie the table"; + }; + } + + /** + * Hide subcontainer while child inside the table stays visible. + */ + function test5(aContainerID, aSubContainerID, aChildID) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode(aSubContainerID)), + new invokerChecker(EVENT_SHOW, getNode(aChildID)), + new invokerChecker(EVENT_REORDER, getNode(aContainerID)), + ]; + + this.invoke = function invoke() { + var tree = + { SECTION: [ // container + { SECTION: [ // subcontainer + { TABLE: [ + { ROW: [ + { CELL: [ + { SECTION: [ // child + { TEXT_LEAF: [] }, + ] }, + ] }, + ] }, + ] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + + getNode(aSubContainerID).style.visibility = "hidden"; + }; + + this.finalCheck = function finalCheck() { + var tree = + { SECTION: [ // container + { SECTION: [ // child + { TEXT_LEAF: [] }, + ] }, + ] }; + testAccessibleTree(aContainerID, tree); + }; + + this.getID = function getID() { + return "hide subcontainer while child inside the table stays visible"; + }; + } + + /** + * Hide subcontainer while its child and child inside the nested table stays visible. + */ + function test6(aContainerID, aSubContainerID, aChildID, aChild2ID) { + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, getNode(aSubContainerID)), + new invokerChecker(EVENT_SHOW, getNode(aChildID)), + new invokerChecker(EVENT_SHOW, getNode(aChild2ID)), + new invokerChecker(EVENT_REORDER, getNode(aContainerID)), + ]; + + this.invoke = function invoke() { + var tree = + { SECTION: [ // container + { SECTION: [ // subcontainer + { TABLE: [ + { ROW: [ + { CELL: [ + { TABLE: [ // nested table + { ROW: [ + { CELL: [ + { SECTION: [ // child + { TEXT_LEAF: [] } ]} ]} ]} ]} ]} ]} ]}, + { SECTION: [ // child2 + { TEXT_LEAF: [] } ]} ]} ]}; + + testAccessibleTree(aContainerID, tree); + + // invoke + getNode(aSubContainerID).style.visibility = "hidden"; + }; + + this.finalCheck = function finalCheck() { + var tree = + { SECTION: [ // container + { SECTION: [ // child + { TEXT_LEAF: [] } ]}, + { SECTION: [ // child2 + { TEXT_LEAF: [] } ]} ]}; + + testAccessibleTree(aContainerID, tree); + }; + + this.getID = function getID() { + return "hide subcontainer while its child and child inside the nested table stays visible"; + }; + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTest() { + gQueue = new eventQueue(); + + gQueue.push(new test1("t1_container", "t1_parent", "t1_child")); + gQueue.push(new test2("t2_container", "t2_grandparent", "t2_child", "t2_child2")); + gQueue.push(new test3("t3_container", "t3_parent", "t3_parent2", "t3_child", "t3_child2")); + gQueue.push(new test4("t4_container", "t4_child")); + gQueue.push(new test5("t5_container", "t5_subcontainer", "t5_child")); + gQueue.push(new test6("t6_container", "t6_subcontainer", "t6_child", "t6_child2")); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Develop a way to handle visibility style" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=606125"> + Mozilla Bug 606125 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- 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> + <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> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeupdate/test_whitespace.html b/accessible/tests/mochitest/treeupdate/test_whitespace.html new file mode 100644 index 0000000000..ebb199cfbe --- /dev/null +++ b/accessible/tests/mochitest/treeupdate/test_whitespace.html @@ -0,0 +1,200 @@ +<!DOCTYPE html> +<html> + +<head> + <title>Whitespace text accessible creation/destruction</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../role.js"></script> + <script type="application/javascript" + src="../events.js"></script> + + <script type="application/javascript"> + + // ////////////////////////////////////////////////////////////////////////// + // Invokers + + /** + * Middle image accessible removal results in text accessible removal. + * + * Before: + * DOM: whitespace img1 whitespace img2 whitespace img3 whitespace, + * a11y: img1 whitespace img2 whitespace img3 + * After: + * DOM: whitespace img1 whitespace whitespace img3 whitespace, + * a11y: img1 whitespace img3 + */ + function removeImg() { + this.containerNode = getNode("container1"); + this.imgNode = getNode("img1"); + this.img = getAccessible(this.imgNode); + this.text = this.img.nextSibling; + + this.eventSeq = [ + new invokerChecker(EVENT_HIDE, this.img), + new invokerChecker(EVENT_HIDE, this.text), + new invokerChecker(EVENT_REORDER, this.containerNode), + ]; + + this.finalCheck = function textLeafUpdate_finalCheck() { + var tree = + { SECTION: [ + { GRAPHIC: [] }, + { TEXT_LEAF: [] }, + { GRAPHIC: [] }, + ] }; + + testAccessibleTree(this.containerNode, tree); + }; + + this.invoke = function setOnClickAttr_invoke() { + var tree = + { SECTION: [ + { GRAPHIC: [] }, + { TEXT_LEAF: [] }, + { GRAPHIC: [] }, + { TEXT_LEAF: [] }, + { GRAPHIC: [] }, + ] }; + + testAccessibleTree(this.containerNode, tree); + + this.containerNode.removeChild(this.imgNode); + }; + + this.getID = function setOnClickAttr_getID() { + return "remove middle img"; + }; + } + + /** + * Append image making the whitespace visible and thus accessible. + * Note: images and whitespaces are on different leves of accessible trees, + * so that image container accessible update doesn't update the tree + * of whitespace container. + * + * Before: + * DOM: whitespace emptylink whitespace linkwithimg whitespace + * a11y: emptylink linkwithimg + * After: + * DOM: whitespace linkwithimg whitespace linkwithimg whitespace + * a11y: linkwithimg whitespace linkwithimg + */ + function insertImg() { + this.containerNode = getNode("container2"); + this.topNode = this.containerNode.parentNode; + this.textNode = this.containerNode.nextSibling; + this.imgNode = document.createElement("img"); + this.imgNode.setAttribute("src", "../moz.png"); + + this.eventSeq = [ + new asyncInvokerChecker(EVENT_SHOW, getAccessible, this.textNode), + new asyncInvokerChecker(EVENT_SHOW, getAccessible, this.imgNode), + new orderChecker(), + new invokerChecker(EVENT_REORDER, this.topNode), + ]; + + this.invoke = function insertImg_invoke() { + var tree = + { SECTION: [ + { LINK: [] }, + { LINK: [ + { GRAPHIC: [] }, + ] }, + ] }; + + testAccessibleTree(this.topNode, tree); + + this.containerNode.appendChild(this.imgNode); + }; + + this.finalCheck = function insertImg_finalCheck() { + var tree = + { SECTION: [ + { LINK: [ + { GRAPHIC: [ ] }, + ] }, + { TEXT_LEAF: [ ] }, + { LINK: [ + { GRAPHIC: [ ] }, + ] }, + ] }; + + testAccessibleTree(this.topNode, tree); + }; + + this.getID = function appendImg_getID() { + return "insert img into internal container"; + }; + } + + function dontCreateMapWhiteSpace() { + const tree = { SECTION: [ { role: ROLE_TEXT_LEAF, name: "x" } ] }; + testAccessibleTree("container3", tree); + + getNode("c3_inner").style.textAlign = "center"; + document.body.offsetTop; // Flush layout. + window.windowUtils.advanceTimeAndRefresh(100); + + testAccessibleTree("container3", tree); + window.windowUtils.restoreNormalRefresh(); + } + + // ////////////////////////////////////////////////////////////////////////// + // Test + + // gA11yEventDumpID = "eventdump"; // debug stuff + // gA11yEventDumpToConsole = true; + + var gQueue = null; + + function doTest() { + dontCreateMapWhiteSpace(); + + gQueue = new eventQueue(); + + gQueue.push(new removeImg()); + gQueue.push(new insertImg()); + + gQueue.invoke(); // SimpleTest.finish() will be called in the end + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> +</head> +<body> + + <a target="_blank" + title="Make sure accessible tree is correct when rendered text is changed" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=625652"> + Mozilla Bug 625652 + </a> + + <p id="display"></p> + <div id="content" style="display: none"></div> + <pre id="test"> + </pre> + + <!-- Whitespace between the div and img tags will be inconsistent depending + on the image cache state and what optimizations layout was able to + apply. --> + <div id="container1"><img src="../moz.png"> <img id="img1" src="../moz.png"> <img src="../moz.png"></div> + <div><a id="container2"></a> <a><img src="../moz.png"></a></div> + + <div id="container3"> + <div id="c3_inner" role="presentation"> + x<map> </map> + </div> + </div> + + <div id="eventdump"></div> +</body> +</html> diff --git a/accessible/tests/mochitest/treeview.css b/accessible/tests/mochitest/treeview.css new file mode 100644 index 0000000000..f93e318b64 --- /dev/null +++ b/accessible/tests/mochitest/treeview.css @@ -0,0 +1,15 @@ +treechildren::-moz-tree-checkbox(checked) { + list-style-image: url("chrome://global/skin/icons/check.svg"); +} + +treechildren::-moz-tree-image(cyclerState1) { + list-style-image: url("chrome://global/skin/console/bullet-question.png"); +} + +treechildren::-moz-tree-image(cyclerState2) { + list-style-image: url("chrome://global/skin/console/bullet-warning.png"); +} + +treechildren::-moz-tree-image(cyclerState3) { + list-style-image: url("chrome://global/skin/console/bullet-error.png"); +} diff --git a/accessible/tests/mochitest/treeview.js b/accessible/tests/mochitest/treeview.js new file mode 100644 index 0000000000..df2468a37a --- /dev/null +++ b/accessible/tests/mochitest/treeview.js @@ -0,0 +1,273 @@ +/* import-globals-from common.js */ +/* import-globals-from events.js */ + +/** + * Helper method to start a single XUL tree test. + */ +function loadXULTreeAndDoTest(aDoTestFunc, aTreeID, aTreeView) { + var doTestFunc = aDoTestFunc ? aDoTestFunc : gXULTreeLoadContext.doTestFunc; + var treeID = aTreeID ? aTreeID : gXULTreeLoadContext.treeID; + var treeView = aTreeView ? aTreeView : gXULTreeLoadContext.treeView; + + let treeNode = getNode(treeID); + + gXULTreeLoadContext.queue = new eventQueue(); + gXULTreeLoadContext.queue.push({ + treeNode, + + eventSeq: [new invokerChecker(EVENT_REORDER, treeNode)], + + invoke() { + this.treeNode.view = treeView; + }, + + getID() { + return "Load XUL tree " + prettyName(treeID); + }, + }); + gXULTreeLoadContext.queue.onFinish = function() { + SimpleTest.executeSoon(doTestFunc); + return DO_NOT_FINISH_TEST; + }; + gXULTreeLoadContext.queue.invoke(); +} + +/** + * Analogy of addA11yLoadEvent, nice helper to load XUL tree and start the test. + */ +function addA11yXULTreeLoadEvent(aDoTestFunc, aTreeID, aTreeView) { + gXULTreeLoadContext.doTestFunc = aDoTestFunc; + gXULTreeLoadContext.treeID = aTreeID; + gXULTreeLoadContext.treeView = aTreeView; + + addA11yLoadEvent(loadXULTreeAndDoTest); +} + +function nsTableTreeView(aRowCount) { + this.__proto__ = new nsTreeView(); + + for (var idx = 0; idx < aRowCount; idx++) { + this.mData.push(new treeItem("row" + String(idx) + "_")); + } +} + +function nsTreeTreeView() { + this.__proto__ = new nsTreeView(); + + this.mData = [ + new treeItem("row1"), + new treeItem("row2_", true, [ + new treeItem("row2.1_"), + new treeItem("row2.2_"), + ]), + new treeItem("row3_", false, [ + new treeItem("row3.1_"), + new treeItem("row3.2_"), + ]), + new treeItem("row4"), + ]; +} + +function nsTreeView() { + this.mTree = null; + this.mData = []; +} + +nsTreeView.prototype = { + // //////////////////////////////////////////////////////////////////////////// + // nsITreeView implementation + + get rowCount() { + return this.getRowCountIntl(this.mData); + }, + setTree: function setTree(aTree) { + this.mTree = aTree; + }, + getCellText: function getCellText(aRow, aCol) { + var data = this.getDataForIndex(aRow); + if (aCol.id in data.colsText) { + return data.colsText[aCol.id]; + } + + return data.text + aCol.id; + }, + getCellValue: function getCellValue(aRow, aCol) { + var data = this.getDataForIndex(aRow); + return data.value; + }, + getRowProperties: function getRowProperties(aIndex) { + return ""; + }, + getCellProperties: function getCellProperties(aIndex, aCol) { + if (!aCol.cycler) { + return ""; + } + + var data = this.getDataForIndex(aIndex); + return this.mCyclerStates[data.cyclerState]; + }, + getColumnProperties: function getColumnProperties(aCol) { + return ""; + }, + getParentIndex: function getParentIndex(aRowIndex) { + var info = this.getInfoByIndex(aRowIndex); + return info.parentIndex; + }, + hasNextSibling: function hasNextSibling(aRowIndex, aAfterIndex) {}, + getLevel: function getLevel(aIndex) { + var info = this.getInfoByIndex(aIndex); + return info.level; + }, + getImageSrc: function getImageSrc(aRow, aCol) {}, + isContainer: function isContainer(aIndex) { + var data = this.getDataForIndex(aIndex); + return data.open != undefined; + }, + isContainerOpen: function isContainerOpen(aIndex) { + var data = this.getDataForIndex(aIndex); + return data.open; + }, + isContainerEmpty: function isContainerEmpty(aIndex) { + var data = this.getDataForIndex(aIndex); + return data.open == undefined; + }, + isSeparator: function isSeparator(aIndex) {}, + isSorted: function isSorted() {}, + toggleOpenState: function toggleOpenState(aIndex) { + var data = this.getDataForIndex(aIndex); + + data.open = !data.open; + var rowCount = this.getRowCountIntl(data.children); + + if (data.open) { + this.mTree.rowCountChanged(aIndex + 1, rowCount); + } else { + this.mTree.rowCountChanged(aIndex + 1, -rowCount); + } + }, + selectionChanged: function selectionChanged() {}, + cycleHeader: function cycleHeader(aCol) {}, + cycleCell: function cycleCell(aRow, aCol) { + var data = this.getDataForIndex(aRow); + data.cyclerState = (data.cyclerState + 1) % 3; + + this.mTree.invalidateCell(aRow, aCol); + }, + isEditable: function isEditable(aRow, aCol) { + return true; + }, + setCellText: function setCellText(aRow, aCol, aValue) { + var data = this.getDataForIndex(aRow); + data.colsText[aCol.id] = aValue; + }, + setCellValue: function setCellValue(aRow, aCol, aValue) { + var data = this.getDataForIndex(aRow); + data.value = aValue; + + this.mTree.invalidateCell(aRow, aCol); + }, + + // //////////////////////////////////////////////////////////////////////////// + // public implementation + + appendItem: function appendItem(aText) { + this.mData.push(new treeItem(aText)); + }, + + // //////////////////////////////////////////////////////////////////////////// + // private implementation + + getDataForIndex: function getDataForIndex(aRowIndex) { + return this.getInfoByIndex(aRowIndex).data; + }, + + getInfoByIndex: function getInfoByIndex(aRowIndex) { + var info = { + data: null, + parentIndex: -1, + level: 0, + index: -1, + }; + + this.getInfoByIndexIntl(aRowIndex, info, this.mData, 0); + return info; + }, + + getRowCountIntl: function getRowCountIntl(aChildren) { + var rowCount = 0; + for (var childIdx = 0; childIdx < aChildren.length; childIdx++) { + rowCount++; + + var data = aChildren[childIdx]; + if (data.open) { + rowCount += this.getRowCountIntl(data.children); + } + } + + return rowCount; + }, + + getInfoByIndexIntl: function getInfoByIndexIntl( + aRowIdx, + aInfo, + aChildren, + aLevel + ) { + var rowIdx = aRowIdx; + for (var childIdx = 0; childIdx < aChildren.length; childIdx++) { + var data = aChildren[childIdx]; + + aInfo.index++; + + if (rowIdx == 0) { + aInfo.data = data; + aInfo.level = aLevel; + return -1; + } + + if (data.open) { + var parentIdx = aInfo.index; + rowIdx = this.getInfoByIndexIntl( + rowIdx - 1, + aInfo, + data.children, + aLevel + 1 + ); + + if (rowIdx == -1) { + if (aInfo.parentIndex == -1) { + aInfo.parentIndex = parentIdx; + } + return 0; + } + } else { + rowIdx--; + } + } + + return rowIdx; + }, + + mCyclerStates: ["cyclerState1", "cyclerState2", "cyclerState3"], +}; + +function treeItem(aText, aOpen, aChildren) { + this.text = aText; + this.colsText = {}; + this.open = aOpen; + this.value = "true"; + this.cyclerState = 0; + if (aChildren) { + this.children = aChildren; + } +} + +/** + * Used in conjunction with loadXULTreeAndDoTest and addA11yXULTreeLoadEvent. + */ +var gXULTreeLoadContext = { + doTestFunc: null, + treeID: null, + treeView: null, + queue: null, +}; diff --git a/accessible/tests/mochitest/value.js b/accessible/tests/mochitest/value.js new file mode 100644 index 0000000000..379403ecaf --- /dev/null +++ b/accessible/tests/mochitest/value.js @@ -0,0 +1,52 @@ +/* import-globals-from common.js */ + +// ////////////////////////////////////////////////////////////////////////////// +// Public methods + +/** + * Tests nsIAccessibleValue interface. + * + * @param aAccOrElmOrId [in] identifier of accessible + * @param aValue [in] accessible value (nsIAccessible::value) + * @param aCurrValue [in] current value (nsIAccessibleValue::currentValue) + * @param aMinValue [in] minimum value (nsIAccessibleValue::minimumValue) + * @param aMaxValue [in] maximumn value (nsIAccessibleValue::maximumValue) + * @param aMinIncr [in] minimum increment value + * (nsIAccessibleValue::minimumIncrement) + */ +function testValue( + aAccOrElmOrId, + aValue, + aCurrValue, + aMinValue, + aMaxValue, + aMinIncr +) { + var acc = getAccessible(aAccOrElmOrId, [nsIAccessibleValue]); + if (!acc) { + return; + } + + is(acc.value, aValue, "Wrong value of " + prettyName(aAccOrElmOrId)); + + is( + acc.currentValue, + aCurrValue, + "Wrong current value of " + prettyName(aAccOrElmOrId) + ); + is( + acc.minimumValue, + aMinValue, + "Wrong minimum value of " + prettyName(aAccOrElmOrId) + ); + is( + acc.maximumValue, + aMaxValue, + "Wrong maximum value of " + prettyName(aAccOrElmOrId) + ); + is( + acc.minimumIncrement, + aMinIncr, + "Wrong minimum increment value of " + prettyName(aAccOrElmOrId) + ); +} diff --git a/accessible/tests/mochitest/value/a11y.ini b/accessible/tests/mochitest/value/a11y.ini new file mode 100644 index 0000000000..8ebcf83f0c --- /dev/null +++ b/accessible/tests/mochitest/value/a11y.ini @@ -0,0 +1,9 @@ +[DEFAULT] +support-files = + !/accessible/tests/mochitest/*.js + +[test_ariavalue.html] +[test_general.html] +[test_number.html] +[test_progress.html] +[test_range.html] diff --git a/accessible/tests/mochitest/value/test_ariavalue.html b/accessible/tests/mochitest/value/test_ariavalue.html new file mode 100644 index 0000000000..fa240e082e --- /dev/null +++ b/accessible/tests/mochitest/value/test_ariavalue.html @@ -0,0 +1,68 @@ +<html> + +<head> + <title>nsIAccessible value testing for implicit aria-value* attributes</title> + + <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" src="../common.js"></script> + <script type="application/javascript" src="../value.js"></script> + + <script src="chrome://mochikit/content/chrome-harness.js"></script> + + <script type="application/javascript"> + function doTest() { + for (const role of ["slider", "scrollbar"]) { + testValue(`${role}_default`, "50", 50, 0, 100, 0); + testValue(`${role}_min1max50`, "25.5", 25.5, 1, 50, 0); + testValue(`${role}_max200`, "100", 100, 0, 200, 0); + testValue(`${role}_min10`, "55", 55, 10, 100, 0); + testValue(`${role}_vt`, "juice", 50, 0, 100, 0); + testValue(`${role}_vn`, "6", 6, 0, 100, 0); + testValue(`${role}_vtvn`, "juice", 6, 0, 100, 0); + } + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1357071" + title="Add support for implicit values for aria-value* attributes for scrollbar and slider roles"> + Bug 1357071 + </a> + + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + + <!-- ARIA sliders --> + <div id="slider_default" role="slider">vanilla slider</div> + <div id="slider_min1max50" role="slider" aria-valuemin="1" aria-valuemax="50">banana slider</div> + <div id="slider_max200" role="slider" aria-valuemax="200">cherry slider</div> + <div id="slider_min10" role="slider" aria-valuemin="10">strawberry slider</div> + <div id="slider_vt" role="slider" aria-valuetext="juice">orange slider</div> + <div id="slider_vn" role="slider" aria-valuenow="6">chocolate slider</div> + <div id="slider_vtvn" role="slider" aria-valuetext="juice" aria-valuenow="6">apple slider</div> + + <!-- ARIA scrollbars --> + <div id="scrollbar_default" role="scrollbar">vanilla scrollbar</div> + <div id="scrollbar_min1max50" role="scrollbar" aria-valuemin="1" aria-valuemax="50">banana scrollbar</div> + <div id="scrollbar_max200" role="scrollbar" aria-valuemax="200">cherry scrollbar</div> + <div id="scrollbar_min10" role="scrollbar" aria-valuemin="10">strawberry scrollbar</div> + <div id="scrollbar_vt" role="scrollbar" aria-valuetext="juice">orange scrollbar</div> + <div id="scrollbar_vn" role="scrollbar" aria-valuenow="6">chocolate scrollbar</div> + <div id="scrollbar_vtvn" role="scrollbar" aria-valuetext="juice" aria-valuenow="6">apple scrollbar</div> +</body> + +</html> diff --git a/accessible/tests/mochitest/value/test_general.html b/accessible/tests/mochitest/value/test_general.html new file mode 100644 index 0000000000..862c5254c0 --- /dev/null +++ b/accessible/tests/mochitest/value/test_general.html @@ -0,0 +1,159 @@ +<html> + +<head> + <title>nsIAccessible value testing</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <style type="text/css"> + .offscreen { + position: absolute; + left: -5000px; + top: -5000px; + height: 100px; + width: 100px; + } + </style> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + + <script src="chrome://mochikit/content/chrome-harness.js"></script> + + <script type="application/javascript"> + function doTest() { + function testValue(aID, aValue) { + var acc = getAccessible(aID); + if (!acc) + return; + is(acc.value, aValue, "Wrong value for " + aID + "!"); + } + + var href = getRootDirectory(window.location.href) + "foo"; + + // roles that can't live as HTMLLinkAccessibles + testValue("aria_menuitem_link", ""); + testValue("aria_button_link", ""); + testValue("aria_checkbox_link", ""); + testValue("aria_application_link", ""); + testValue("aria_main_link", ""); + testValue("aria_navigation_link", ""); + + // roles that can live as HTMLLinkAccessibles + testValue("aria_link_link", href); + + // //////////////////////////////////////////////////////////////////////// + // ARIA textboxes + + testValue("aria_textbox1", "helo"); + // Textbox containing list. + testValue("aria_textbox2", "1. test"); + + // //////////////////////////////////////////////////////////////////////// + // ARIA comboboxes + + // aria-activedescendant defines a current item the value is computed from + testValue("aria_combobox1", kDiscBulletText + "Zoom"); + + // aria-selected defines a selected item the value is computed from, + // list control is pointed by aria-owns relation. + testValue("aria_combobox2", kDiscBulletText + "Zoom"); + + // aria-selected defines a selected item the value is computed from, + // list control is a child of combobox. + testValue("aria_combobox3", kDiscBulletText + "2"); + + // //////////////////////////////////////////////////////////////////////// + // HTML controls + testValue("combobox1", "item1"); + testValue("combobox2", "item2"); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=494807" + title="Do not expose a11y info specific to hyperlinks when role is overridden using ARIA"> + Bug 494807 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=819273" + title="ARIA combobox should have accessible value"> + Bug 819273 + </a> + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=887250" + title="ARIA textbox role doesn't expose value"> + Bug 887250 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + + <a id="aria_menuitem_link" role="menuitem" href="foo">menuitem</a> + <a id="aria_button_link" role="button" href="foo">button</a> + <a id="aria_checkbox_link" role="checkbox" href="foo">checkbox</a> + + <!-- landmark links --> + <a id="aria_application_link" role="application" href="foo">app</a> + <a id="aria_main_link" role="main" href="foo">main</a> + <a id="aria_navigation_link" role="navigation" href="foo">nav</a> + + <!-- strange edge case: please don't do this in the wild --> + <a id="aria_link_link" role="link" href="foo">link</a> + + <div id="aria_textbox1" role="textbox">helo</div> + <div id="aria_textbox2" contenteditable role="textbox"> + <ol><li>test</li></ol> + </div> + + <div id="aria_combobox1" role="combobox" + aria-owns="aria_combobox1_owned_listbox" + aria-activedescendant="aria_combobox1_selected_option"> + </div> + <ul role="listbox" id="aria_combobox1_owned_listbox"> + <li role="option">Zebra</li> + <li role="option" id="aria_combobox1_selected_option">Zoom</li> + </ul> + + <div id="aria_combobox2" role="combobox" + aria-owns="aria_combobox2_owned_listbox"> + </div> + <ul role="listbox" id="aria_combobox2_owned_listbox"> + <li role="option">Zebra</li> + <li role="option" aria-selected="true">Zoom</li> + </ul> + + <div id="aria_combobox3" role="combobox"> + <div role="textbox"></div> + <ul role="listbox"> + <li role="option">1</li> + <li role="option" aria-selected="true">2</li> + <li role="option">3</li> + </ul> + </div> + + <select id="combobox1"> + <option id="cb1_item1">item1</option> + <option id="cb1_item2">item2</option> + </select> + <select id="combobox2"> + <option id="cb2_item1">item1</option> + <option id="cb2_item2" selected="true">item2</option> + </select> + +</body> +</html> diff --git a/accessible/tests/mochitest/value/test_number.html b/accessible/tests/mochitest/value/test_number.html new file mode 100644 index 0000000000..59024c3ef3 --- /dev/null +++ b/accessible/tests/mochitest/value/test_number.html @@ -0,0 +1,56 @@ +<html> + +<head> + <title>nsIAccessible value testing for input@type=range element</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../value.js"></script> + + <script src="chrome://mochikit/content/chrome-harness.js"></script> + + <script type="application/javascript"> + function doTest() { + // HTML5 number element tests + testValue("number", "", 0, 0, 0, 1); + testValue("number_value", "1", 1, 0, 0, 1); + testValue("number_step", "", 0, 0, 0, 1); + testValue("number_min42", "", 0, 42, 0, 1); + testValue("number_max42", "", 0, 0, 42, 1); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559761" + title="make HTML5 input@type=number element accessible"> + Bug 559761 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + + <!-- HTML5 input@type=number element --> + <input type="number" id="number"> + <input type="number" id="number_value" value="1"> + <input type="number" id="number_step" step="1"> + <input type="number" id="number_min42" min="42"> + <input type="number" id="number_max42" max="42"> +</body> +</html> diff --git a/accessible/tests/mochitest/value/test_progress.html b/accessible/tests/mochitest/value/test_progress.html new file mode 100644 index 0000000000..03ddb02196 --- /dev/null +++ b/accessible/tests/mochitest/value/test_progress.html @@ -0,0 +1,61 @@ +<html> + +<head> + <title>nsIAccessible value testing for progress element</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../value.js"></script> + + <script src="chrome://mochikit/content/chrome-harness.js"></script> + + <script type="application/javascript"> + function doTest() { + // HTML5 progress element tests + testValue("pr_indeterminate", "", 0, 0, 1, 0); + testValue("pr_zero", "0%", 0, 0, 1, 0); + testValue("pr_zeropointfive", "50%", 0.5, 0, 1, 0); + testValue("pr_one", "100%", 1, 0, 1, 0); + testValue("pr_42", "100%", 42, 0, 1, 0); + testValue("pr_21", "50%", 21, 0, 42, 0); + testValue("pr_valuetext", "value", 0, 0, 1, 0); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559773" + title="make HTML5 progress element accessible"> + Mozilla Bug 559773 + </a><br /> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + + <!-- HTML5 progress element --> + <progress id="pr_indeterminate">this will be read by legacy browsers</progress> + <progress id="pr_zero" value="0">this will be read by legacy browsers</progress> + <progress id="pr_zeropointfive" value="0.5">this will be read by legacy browsers</progress> + <progress id="pr_one" value="1">this will be read by legacy browsers</progress> + <progress id="pr_42" value="42">this will be read by legacy browsers</progress> + <progress id="pr_21" value="21" max="42">this will be read by legacy browsers</progress> + <!-- aria-valuetext should work due to implicit progressbar role (bug 1475376) --> + <progress id="pr_valuetext" aria-valuetext="value">this will be read by legacy browsers</progress> +</body> +</html> diff --git a/accessible/tests/mochitest/value/test_range.html b/accessible/tests/mochitest/value/test_range.html new file mode 100644 index 0000000000..55cb0b1767 --- /dev/null +++ b/accessible/tests/mochitest/value/test_range.html @@ -0,0 +1,59 @@ +<html> + +<head> + <title>nsIAccessible value testing for input@type=range element</title> + + <link rel="stylesheet" type="text/css" + href="chrome://mochikit/content/tests/SimpleTest/test.css" /> + + <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> + + <script type="application/javascript" + src="../common.js"></script> + <script type="application/javascript" + src="../value.js"></script> + + <script src="chrome://mochikit/content/chrome-harness.js"></script> + + <script type="application/javascript"> + function doTest() { + // HTML5 progress element tests + testValue("range", "50", 50, 0, 100, 1); + testValue("range_value", "1", 1, 0, 100, 1); + testValue("range_step", "50", 50, 0, 100, 1); + testValue("range_min42", "71", 71, 42, 100, 1); + testValue("range_max42", "21", 21, 0, 42, 1); + testValue("range_valuetext", "value", 50, 0, 100, 1); + + SimpleTest.finish(); + } + + SimpleTest.waitForExplicitFinish(); + addA11yLoadEvent(doTest); + </script> + +</head> + +<body> + + <a target="_blank" + href="https://bugzilla.mozilla.org/show_bug.cgi?id=559764" + title="make HTML5 input@type=range element accessible"> + Bug 559764 + </a> + <p id="display"></p> + <div id="content" style="display: none"> + </div> + <pre id="test"> + </pre> + + <!-- HTML5 input@type=range element --> + <input type="range" id="range"> + <input type="range" id="range_value" value="1"> + <input type="range" id="range_step" step="1"> + <input type="range" id="range_min42" min="42"> + <input type="range" id="range_max42" max="42"> + <!-- aria-valuetext should work due to implicit slider role (bug 1475376) --> + <input type="range" id="range_valuetext" aria-valuetext="value"> +</body> +</html> |