/* 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/layout.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, untilCacheIs, untilCacheOk, testBoundsWithContent, waitForContentPaint, runPython */ 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. */ // eslint-disable-next-line @microsoft/sdl/no-insecure-url 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() { 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, symbol } of scripts) { let contentScript = `${CURRENT_DIR}${script}`; 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, symbol], async (_contentScript, importSymbol) => { let module = ChromeUtils.importESModule(_contentScript); content.window[importSymbol] = module[importSymbol]; } ); 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) { // eslint-disable-next-line @microsoft/sdl/no-insecure-url 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", ` Accessibility Fission Test ${doc} ` ); } 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( //, `` ); } else { doc = ` ${doc}`; } src = `data:${mimeType};charset=utf-8,${encodeURIComponent(doc)}`; } iframeAttrs = { id: DEFAULT_IFRAME_ID, src, ...iframeAttrs, }; return `