Mypal68/accessible/tests/browser/shared-head.js
2025-04-19 19:21:07 +03:00

431 lines
12 KiB
JavaScript

/* 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, loadFrameScripts, snippetToURL,
Cc, Cu, arrayFromChildren, forceGC, contentSpawnMutation */
/**
* Current browser test directory path used to load subscripts.
*/
const CURRENT_DIR =
"chrome://mochitests/content/browser/accessible/tests/browser/";
/**
* 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/browser/accessible/tests/browser/";
const LOADED_FRAMESCRIPTS = new Map();
/**
* 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 ContentTask.spawn(
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).
* @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 ContentTask.spawn(
browser,
[id, style, value],
([contentId, contentStyle, contentValue]) => {
let 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).
* @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 ContentTask.spawn(browser, id, contentId => {
let elm = content.document.getElementById(contentId);
if (elm.editor) {
elm.selectionStart = elm.selectionEnd = elm.value.length;
}
elm.focus();
});
}
/**
* 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 frame scripts into test's content.
* @param {Object} browser browser element that content belongs to
* @param {Array} scripts a list of scripts to load into content
*/
function loadFrameScripts(browser, ...scripts) {
let mm = browser.messageManager;
for (let script of scripts) {
let frameScript;
if (typeof script === "string") {
if (script.includes(".js")) {
// If script string includes a .js extention, assume it is a script
// path.
frameScript = `${CURRENT_DIR}${script}`;
} else {
// Otherwise it is a serealized script.
frameScript = `data:,${script}`;
}
} else {
// Script is a object that has { dir, name } format.
frameScript = `${script.dir}${script.name}`;
}
let loadedScriptSet = LOADED_FRAMESCRIPTS.get(frameScript);
if (!loadedScriptSet) {
loadedScriptSet = new WeakSet();
LOADED_FRAMESCRIPTS.set(frameScript, loadedScriptSet);
} else if (loadedScriptSet.has(browser)) {
continue;
}
mm.loadFrameScript(frameScript, false, true);
loadedScriptSet.add(browser);
}
}
/**
* Takes an HTML snippet and returns an encoded URI for a full document
* with the snippet.
* @param {String} snippet a markup snippet.
* @param {Object} bodyAttrs extra attributes to use in the body tag. Default is
* { id: "body "}.
* @return {String} a base64 encoded data url of the document container the
* snippet.
**/
function snippetToURL(snippet, bodyAttrs = {}) {
let attrs = Object.assign({}, { id: "body" }, bodyAttrs);
let attrsString = Object.entries(attrs)
.map(([attr, value]) => `${attr}=${JSON.stringify(value)}`)
.join(" ");
let encodedDoc = encodeURIComponent(
`<html>
<head>
<meta charset="utf-8"/>
<title>Accessibility Test</title>
</head>
<body ${attrsString}>${snippet}</body>
</html>`
);
return `data:text/html;charset=utf-8,${encodedDoc}`;
}
/**
* 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
*/
function addAccessibleTask(doc, task) {
add_task(async function() {
let url;
if (doc.includes("doc_")) {
url = `${CURRENT_CONTENT_DIR}e10s/${doc}`;
} else {
url = snippetToURL(doc);
}
registerCleanupFunction(() => {
for (let observer of Services.obs.enumerateObservers(
"accessible-event"
)) {
Services.obs.removeObserver(observer, "accessible-event");
}
});
let onDocLoad = waitForEvent(EVENT_DOCUMENT_LOAD_COMPLETE, "body");
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);
loadFrameScripts(
browser,
"let { document, window, navigator } = content;",
{ name: "common.js", dir: MOCHITESTS_DIR }
);
Logger.log(
`e10s enabled: ${Services.appinfo.browserTabsRemoteAutostart}`
);
Logger.log(`Actually remote browser: ${browser.isRemoteBrowser}`);
let event = await onDocLoad;
await task(browser, event.accessible);
}
);
});
}
/**
* 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 = null) {
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 ContentTask.spawn(browser, null, tick);
// Perform the tree mutation.
await ContentTask.spawn(browser, args, func);
// Do one tick to flush our queue (insertions, relocations, etc.)
await ContentTask.spawn(browser, null, tick);
let events = await onReorders;
unexpectedListener.stop();
// Go back to normal refresh driver ticks.
await ContentTask.spawn(browser, null, function() {
content.windowUtils.restoreNormalRefresh();
});
return events;
}