/* 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 addPdfStructTreeTest, addPdfOutlineTest, addPdfTabTask, loadPdfTestDoc, exportPdf, assertPdfStructTree, CommonUtils */ // Prevent common.js from instantiating the accessibility service when it is // loaded. These tests need to start with the accessibility service disabled. window.gDisableAccServiceInit = true; Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/accessible/tests/browser/shared-head.js", this ); Services.scriptloader.loadSubScript( "chrome://mochitests/content/browser/toolkit/components/printing/tests/head.js", this ); loadScripts( { name: "common.js", dir: MOCHITESTS_DIR }, { name: "events.js", dir: MOCHITESTS_DIR } ); const { CommonUtils } = ChromeUtils.importESModule( "chrome://mochitests/content/browser/accessible/tests/browser/Common.sys.mjs" ); const pdfjsLib = ChromeUtils.importESModule("resource://pdf.js/build/pdf.mjs"); pdfjsLib.GlobalWorkerOptions.workerSrc = "resource://pdf.js/build/pdf.worker.mjs"; delete window.gDisableAccServiceInit; /** * The PDF struct tree doesn't contain text content itself. Instead, it uses a * node which refers to marked content in the PDF content stream using an id. In * the PDF content stream, marked content is delimited by a begin operator * specifying the id and an end operator. Rather than making individual tests * match up the ids and separately test the struct tree and content items, this * function finds marked content items and inserts their strings directly into a * .content array on the struct tree node. * * Structure ids are generated from a counter covering every Accessible in the * document, so they depend on things the tests don't describe; e.g. they differ * between the topLevel and iframe variants of the same test. They are therefore * renumbered in tree order. A cell can reference a header which comes later in * the tree, so the headers references are remapped after the walk, once all ids * are known. */ function simplifyStructTree(root, contentItems) { const structIds = new Map(); const nodesWithHeaders = []; const walk = node => { if (node.type == "content") { // Find the associated content items and append their strings to // node.content. node.content = []; let inMarked = false; for (const item of contentItems) { if (item.type == "beginMarkedContentProps" && item.id == node.id) { inMarked = true; continue; } if (!inMarked) { continue; } if (item.str) { node.content.push(item.str); continue; } if (item.type == "endMarkedContent") { break; } } delete node.type; delete node.id; } if (node.structId) { const newId = `id${structIds.size + 1}`; structIds.set(node.structId, newId); node.structId = newId; } if (node.headers) { nodesWithHeaders.push(node); } for (const child of node.children || []) { walk(child); } }; walk(root); for (const node of nodesWithHeaders) { node.headers = node.headers.map(id => structIds.get(id) || id); } } /** * PDF outline nodes contain a lot of properties we can't or don't want to test * yet. Remove any properties we're not interested in. */ function simplifyOutlineNode(node) { for (const key in node) { if (!["items", "title"].includes(key)) { delete node[key]; } } for (const child of node.items) { simplifyOutlineNode(child); } } /** * Similar to addAccessibleTask, schedule one add_task per requested variant. * Unlike addAccessibleTask, this does *not* initialize the accessibility * service or wait for accessibility events. Each task opens a fresh tab and * calls `task(ctx)`, where `ctx` contains { tab, browser, variant }. The task * is responsible for loading a document (loadPdfTestDoc), exporting (exportPdf) * and any pre/post assertions. * * `options` is similar to addAccessibleTask: topLevel, chrome, iframe, * remoteIframe. */ function addPdfTabTask(task, options = {}) { const { topLevel = true, chrome = false, iframe = false, remoteIframe = false, } = options; const variants = []; if (topLevel) { variants.push({ suffix: "_topLevel" }); } if (chrome) { variants.push({ suffix: "_chrome", chrome: true }); } if (iframe) { variants.push({ suffix: "_iframe", iframe: true }); } if (remoteIframe) { variants.push({ suffix: "_remoteIframe", remoteIframe: true }); } for (const variant of variants) { const wrapped = async function () { const tabOpts = variant.chrome ? { allowInheritPrincipal: true, forceNotRemote: true } : {}; // Open a blank tab first so we can install the necessary load listeners // before navigating, and so we can apply the tweaks needed for chrome // documents before triggering the real load. gBrowser.selectedTab = BrowserTestUtils.addTab( gBrowser, "about:blank", tabOpts ); const tab = gBrowser.selectedTab; const browser = tab.linkedBrowser; registerCleanupFunction(() => { if (tab && !tab.closing && tab.linkedBrowser) { gBrowser.removeTab(tab); } }); if (variant.chrome) { await SpecialPowers.pushPrefEnv({ set: [["security.allow_unsafe_parent_loads", true]], }); // Ensure this never becomes a remote browser. browser.removeAttribute("maychangeremoteness"); } await task({ tab, browser, variant }); gBrowser.removeTab(tab); }; // Propagate the test name to the wrapper so it shows up in test run // output, suffixed with the variant. Object.defineProperty(wrapped, "name", { value: task.name + variant.suffix, }); add_task(wrapped); } } /** * As part of a task using addPdfTabTask, load the snippet `doc` into the tab * represented by `ctx`. `options` is forwarded to snippetToUrl. */ async function loadPdfTestDoc(ctx, doc, options = {}) { const url = snippetToURL(doc, { ...options, iframe: ctx.variant.iframe, remoteIframe: ctx.variant.remoteIframe, }); const useIframe = ctx.variant.iframe || ctx.variant.remoteIframe; const topLoaded = BrowserTestUtils.browserLoaded(ctx.browser); const iframeLoaded = useIframe ? BrowserTestUtils.browserLoaded( ctx.browser, /* includeSubFrames */ true, u => u != "about:blank" && u != url ) : null; if (ctx.variant.chrome) { ctx.browser.setAttribute("src", url); } else { BrowserTestUtils.startLoadingURIString(ctx.browser, url); } await topLoaded; if (iframeLoaded) { await iframeLoaded; } await SimpleTest.promiseFocus(ctx.browser); } /** * As part of a task using addPdfTabTask, print the loaded document to a PDF and * return the parsed pdf.js document. */ async function exportPdf(ctx) { const helper = new PrintHelper(ctx.browser); await helper.startPrint(); const file = helper.mockFilePicker("accessible_test.pdf"); await helper.assertPrintToFile(file, () => { helper.click(helper.get("print-button")); }); const data = await IOUtils.read(file.path); file.remove(false); Services.prefs.clearUserPref("print_printer"); return pdfjsLib.getDocument({ data }).promise; } function addPdfTest(testName, doc, task, options = {}) { const body = async ctx => { await loadPdfTestDoc(ctx, doc, options); const pdf = await exportPdf(ctx); await task(pdf); }; Object.defineProperty(body, "name", { value: testName }); addPdfTabTask(body, options); } /** * Assert that the struct tree of `pdf` matches `pageTrees` (one expected tree * per page). */ async function assertPdfStructTree(pdf, pageTrees) { for (let p = 0; p < pageTrees.length; ++p) { const pageNum = p + 1; const page = await pdf.getPage(pageNum); const actualTree = await page.getStructTree(); const contentItems = ( await page.getTextContent({ includeMarkedContent: true }) ).items; simplifyStructTree(actualTree, contentItems); SimpleTest.isDeeply( actualTree, pageTrees[p], `Page ${pageNum} struct tree correct` ); } } /** * Add a PDF struct tree test. * * @param testName The name of the test to show in log output. * @param doc The markup to convert to PDF. * @param pageTrees An array of PDF struct trees for each page of the PDF. * @param options Options to pass to addPdfTest. */ function addPdfStructTreeTest(testName, doc, pageTrees, options = {}) { addPdfTest( testName, doc, pdf => assertPdfStructTree(pdf, pageTrees), options ); } /** * Add a PDF outline test. * * @param testName The name of the test to show in log output. * @param doc The markup to convert to PDF. * @param outline An array of PDF outline node information. * @param options Options to pass to addPdfTest. */ function addPdfOutlineTest(testName, doc, outline, options = {}) { async function task(pdf) { const actualOutline = await pdf.getOutline(); for (const node of actualOutline) { simplifyOutlineNode(node); } SimpleTest.isDeeply(actualOutline, outline, "Outline correct"); } addPdfTest(testName, doc, task, options); }