Files
sousa-gecko/accessible/tests/browser/pdfOutput/head.js
T
James Teh 3eada0a31d Bug 2021820 part 12: Don't start the accessibility engine in pdfOutput tests. r=eeejay
Now that we can export a tagged PDF without instantiating the accessibility engine, our tests shouldn't depend on the engine already being started.
This means these tests can't use addAccessibleTask, which required some new harness functionality to load snippets without depending on accessibility events.
The pdfOutput harness has also been refactored so that tests can be written which assert things before and after PDF export.
This will be necessary in the next patch in order to test accessibility engine creation and shutdown in various situations.

Differential Revision: https://phabricator.services.mozilla.com/D299964
2026-05-21 01:59:41 +00:00

278 lines
8.5 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";
/* 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.
*/
function simplifyStructTreeNode(node, contentItems) {
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.children) {
for (const child of node.children) {
simplifyStructTreeNode(child, contentItems);
}
}
}
/**
* 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;
simplifyStructTreeNode(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);
}