This commit is contained in:
2026-06-08 13:26:57 +02:00
parent be60be5b03
commit 141762041b
2444 changed files with 1179392 additions and 15 deletions

View File

@@ -0,0 +1,610 @@
/* Copyright 2021 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
awaitPromise,
closePages,
loadAndWait,
waitForPageRendered,
} from "./test_utils.mjs";
const isStructTreeVisible = async page => {
await page.waitForSelector(".structTree");
return page.evaluate(() => {
let elem = document.querySelector(".structTree");
while (elem) {
if (elem.getAttribute("aria-hidden") === "true") {
return false;
}
elem = elem.parentElement;
}
return true;
});
};
describe("accessibility", () => {
describe("structure tree", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("structure_simple.pdf", ".structTree");
});
afterEach(async () => {
await closePages(pages);
});
it("must build structure that maps to text layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
expect(await isStructTreeVisible(page))
.withContext(`In ${browserName}`)
.toBeTrue();
// Check the headings match up.
const head1 = await page.$eval(
".structTree [role='heading'][aria-level='1'] span",
el =>
document.getElementById(el.getAttribute("aria-owns")).textContent
);
expect(head1).withContext(`In ${browserName}`).toEqual("Heading 1");
const head2 = await page.$eval(
".structTree [role='heading'][aria-level='2'] span",
el =>
document.getElementById(el.getAttribute("aria-owns")).textContent
);
expect(head2).withContext(`In ${browserName}`).toEqual("Heading 2");
// Check the order of the content.
const texts = await page.$$eval(".structTree [aria-owns]", nodes =>
nodes.map(
el =>
document.getElementById(el.getAttribute("aria-owns"))
.textContent
)
);
expect(texts)
.withContext(`In ${browserName}`)
.toEqual([
"Heading 1",
"This paragraph 1.",
"Heading 2",
"This paragraph 2.",
]);
})
);
});
it("must check that the struct tree is still there after zooming", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
for (let i = 0; i < 8; i++) {
expect(await isStructTreeVisible(page))
.withContext(`In ${browserName}`)
.toBeTrue();
const handle = await waitForPageRendered(page);
await page.click(`#zoom${i < 4 ? "In" : "Out"}Button`);
await awaitPromise(handle);
}
})
);
});
});
describe("Annotation", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey_a11y.pdf",
".textLayer .endOfContent"
);
});
afterEach(async () => {
await closePages(pages);
});
function getSpans(page) {
return page.evaluate(() => {
const elements = document.querySelectorAll(
`.textLayer span[aria-owns]:not([role="presentation"])`
);
const results = [];
for (const element of elements) {
results.push(element.innerText);
}
return results;
});
}
it("must check that some spans are linked to some annotations thanks to aria-owns", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const spanContents = await getSpans(page);
expect(spanContents)
.withContext(`In ${browserName}`)
.toEqual(["Languages", "@intel.com", "Abstract", "Introduction"]);
})
);
});
});
describe("Annotations order", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("fields_order.pdf", ".annotationLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the text fields are in the visual order", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const ids = await page.evaluate(() => {
const elements = document.querySelectorAll(
".annotationLayer .textWidgetAnnotation"
);
const results = [];
for (const element of elements) {
results.push(element.getAttribute("data-annotation-id"));
}
return results;
});
expect(ids)
.withContext(`In ${browserName}`)
.toEqual(["32R", "30R", "31R", "34R", "29R", "33R"]);
})
);
});
});
describe("Stamp annotation accessibility", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tagged_stamp.pdf",
".annotationLayer #pdfjs_internal_id_21R"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check the id in aria-controls", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(".annotationLayer");
const stampId = "pdfjs_internal_id_20R";
await page.click(`#${stampId}`);
const controlledId = await page.$eval(
"#pdfjs_internal_id_21R",
el => document.getElementById(el.getAttribute("aria-controls")).id
);
expect(controlledId)
.withContext(`In ${browserName}`)
.toEqual(stampId);
})
);
});
it("must check the aria-label linked to the stamp annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(".annotationLayer");
const ariaLabel = await page.$eval(
".annotationLayer section[role='img']",
el => el.getAttribute("aria-label")
);
expect(ariaLabel)
.withContext(`In ${browserName}`)
.toEqual("Secondary text for stamp");
})
);
});
it("must check that the stamp annotation is linked to the struct tree", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(".structTree");
const isLinkedToStampAnnotation = await page.$eval(
".structTree [role='figure']",
el =>
document
.getElementById(el.getAttribute("aria-owns"))
.classList.contains("stampAnnotation")
);
expect(isLinkedToStampAnnotation)
.withContext(`In ${browserName}`)
.toEqual(true);
})
);
});
});
describe("Figure in the content stream", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1708040.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that an image is correctly inserted in the text layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
expect(await isStructTreeVisible(page))
.withContext(`In ${browserName}`)
.toBeTrue();
const spanId = await page.evaluate(() => {
const el = document.querySelector(
`.structTree span[role="figure"]`
);
return el.getAttribute("aria-owns") || null;
});
expect(spanId).withContext(`In ${browserName}`).not.toBeNull();
const ariaLabel = await page.evaluate(id => {
const img = document.querySelector(`#${id} > span[role="img"]`);
return img.getAttribute("aria-label");
}, spanId);
expect(ariaLabel)
.withContext(`In ${browserName}`)
.toEqual("A logo of a fox and a globe");
})
);
});
});
describe("No undefined id", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue20102.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that span hasn't an 'undefined' id", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const id = await page.$eval("span.markedContent", span => span.id);
expect(id).withContext(`In ${browserName}`).toBe("");
})
);
});
});
describe("MathML in AF entry from LaTeX", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1937438_af_from_latex.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the MathML is correctly inserted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const mathML = await page.$eval(
"span.structTree span[aria-owns='p58R_mc13'] > math",
el => el?.innerHTML ?? ""
);
expect(mathML)
.withContext(`In ${browserName}`)
.toEqual(
` <msqrt><msup><mi>x</mi><mn>2</mn></msup></msqrt> <mo>=</mo> <mrow intent="absolute-value($x)"><mo>|</mo><mi arg="x">x</mi><mo>|</mo></mrow> `
);
// Check that the math corresponding element is hidden in the text
// layer.
const ariaHidden = await page.$eval("span#p58R_mc13", el =>
el.getAttribute("aria-hidden")
);
expect(ariaHidden).withContext(`In ${browserName}`).toEqual("true");
})
);
});
});
describe("MathML with some attributes in AF entry from LaTeX", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1997343.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the MathML is correctly inserted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const mathML = await page.$eval(
"span.structTree span[aria-owns='p21R_mc64']",
el => el?.innerHTML ?? ""
);
expect(mathML)
.withContext(`In ${browserName}`)
.toEqual(
'<math display="block"> <msup> <mi>𝑛</mi> <mi>𝑝</mi> </msup> <mo lspace="0.278em" rspace="0.278em">=</mo> <mi>𝑛</mi> <mspace width="1.000em"></mspace> <mi> mod </mi> <mspace width="0.167em"></mspace> <mspace width="0.167em"></mspace> <mi>𝑝</mi> </math>'
);
})
);
});
});
describe("MathML tags in the struct tree", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1937438_mml_from_latex.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the MathML is correctly inserted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const mathML = await page.$eval(
"span.structTree span[role='group'] span[role='group']:last-child > span math",
el => el?.innerHTML ?? ""
);
expect(mathML)
.withContext(`In ${browserName}`)
.toEqual(
`<mi aria-owns="p76R_mc16">𝑐</mi><mo aria-owns="p76R_mc17">=</mo><msqrt><mrow><msup><mi aria-owns="p76R_mc18">𝑎</mi><mn aria-owns="p76R_mc19">2</mn></msup><mo aria-owns="p76R_mc20">+</mo><msup><mi aria-owns="p76R_mc21">𝑏</mi><mn aria-owns="p76R_mc22">2</mn></msup></mrow></msqrt>`
);
const ariaHidden = await page.$eval("span#p76R_mc16", el =>
el.getAttribute("aria-hidden")
);
expect(ariaHidden).withContext(`In ${browserName}`).toEqual("true");
})
);
});
});
describe("Artifacts must be aria-hidden", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1937438_mml_from_latex.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that some artifacts are aria-hidden", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const parentSquareRootHidden = await page.evaluate(() => {
for (const span of document.querySelectorAll(".textLayer span")) {
if (span.textContent === "√") {
return span.parentElement.getAttribute("aria-hidden");
}
}
return false;
});
expect(parentSquareRootHidden)
.withContext(`In ${browserName}`)
.toEqual("true");
})
);
});
});
describe("No alt-text with MathML", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug2004951.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that there's no alt-text on the MathML node", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const ariaLabel = await page.$eval(
"span[aria-owns='p3R_mc2']",
el => el.getAttribute("aria-label") || ""
);
expect(ariaLabel).withContext(`In ${browserName}`).toEqual("");
})
);
});
});
describe("Text elements must be aria-hidden when there's MathML and annotations", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug2009627.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the text in text layer is aria-hidden", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const ariaHidden = await page.evaluate(() =>
Array.from(
document.querySelectorAll(".structTree :has(> math)")
).map(el =>
document
.getElementById(el.getAttribute("aria-owns"))
.getAttribute("aria-hidden")
)
);
expect(ariaHidden)
.withContext(`In ${browserName}`)
.toEqual(["true", "true", "true"]);
})
);
});
});
describe("MathML in AF entry with struct tree children must not be duplicated", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug2025674.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the MathML is not duplicated in the struct tree", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// The Formula node has both AF MathML and struct tree children.
// When AF MathML is present, children must not be walked to avoid
// rendering the math content twice in the accessibility tree.
const mathCount = await page.evaluate(
() => document.querySelectorAll(".structTree math").length
);
expect(mathCount).withContext(`In ${browserName}`).toBe(1);
// All text layer elements referenced by the formula subtree must
// be aria-hidden so screen readers don't read both the MathML and
// the underlying text content.
const allHidden = await page.evaluate(() => {
const ids = [];
for (const el of document.querySelectorAll(
".structTree [aria-owns]"
)) {
if (el.closest("math")) {
ids.push(el.getAttribute("aria-owns"));
}
}
// Also collect ids from the formula span itself.
for (const el of document.querySelectorAll(
".structTree span:has(> math)"
)) {
const owned = el.getAttribute("aria-owns");
if (owned) {
ids.push(owned);
}
}
return ids.every(
id =>
document.getElementById(id)?.getAttribute("aria-hidden") ===
"true"
);
});
expect(allHidden).withContext(`In ${browserName}`).toBeTrue();
})
);
});
});
describe("A TH in a TR itself in a TBody is rowheader", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug2014080.pdf", ".textLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the table has the right structure", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
let elementRole = await page.evaluate(() =>
Array.from(
document.querySelector(".structTree [role='table']").children
).map(child => child.getAttribute("role"))
);
// THeader and TBody must be rowgroup.
expect(elementRole)
.withContext(`In ${browserName}`)
.toEqual(["rowgroup", "rowgroup"]);
elementRole = await page.evaluate(() =>
Array.from(
document.querySelector(
".structTree [role='table'] > [role='rowgroup'] > [role='row']"
).children
).map(child => child.getAttribute("role"))
);
// THeader has 3 columnheader.
expect(elementRole)
.withContext(`In ${browserName}`)
.toEqual(["columnheader", "columnheader", "columnheader"]);
elementRole = await page.evaluate(() =>
Array.from(
document.querySelector(
".structTree [role='table'] > [role='rowgroup']:nth-child(2)"
).children
).map(child => child.getAttribute("role"))
);
// TBody has 5 rows.
expect(elementRole)
.withContext(`In ${browserName}`)
.toEqual(["row", "row", "row", "row", "row"]);
elementRole = await page.evaluate(() =>
Array.from(
document.querySelector(
".structTree [role='table'] > [role='rowgroup']:nth-child(2) > [role='row']:first-child"
).children
).map(child => child.getAttribute("role"))
);
// First row has a rowheader and 2 cells.
expect(elementRole)
.withContext(`In ${browserName}`)
.toEqual(["rowheader", "cell", "cell"]);
})
);
});
});
});

View File

@@ -0,0 +1,969 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
closePages,
getAnnotationSelector,
getQuerySelector,
getRect,
getSelector,
loadAndWait,
} from "./test_utils.mjs";
describe("Annotation highlight", () => {
describe("annotation-highlight.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"annotation-highlight.pdf",
getAnnotationSelector("19R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check the popup position in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const highlightSelector = getAnnotationSelector("19R");
const popupSelector = getAnnotationSelector("21R");
const areSiblings = await page.evaluate(
(highlightSel, popupSel) => {
const highlight = document.querySelector(highlightSel);
const popup = document.querySelector(popupSel);
return highlight.nextElementSibling === popup;
},
highlightSelector,
popupSelector
);
expect(areSiblings).withContext(`In ${browserName}`).toEqual(true);
})
);
});
it("must show a popup on mouseover", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
let hidden = await page.$eval(
getAnnotationSelector("21R"),
el => el.hidden
);
expect(hidden).withContext(`In ${browserName}`).toEqual(true);
await page.hover(getAnnotationSelector("19R"));
await page.waitForSelector(getAnnotationSelector("21R"), {
visible: true,
timeout: 0,
});
hidden = await page.$eval(
getAnnotationSelector("21R"),
el => el.hidden
);
expect(hidden).withContext(`In ${browserName}`).toEqual(false);
})
);
});
});
describe("Check that widget annotations are in front of highlight ones", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1883609.pdf", getAnnotationSelector("23R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must click on widget annotations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
for (const i of [23, 22, 14]) {
await page.click(getAnnotationSelector(`${i}R`));
await page.waitForSelector(`#pdfjs_internal_id_${i}R:focus-within`);
}
})
);
});
});
});
describe("Checkbox annotation", () => {
describe("issue12706.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue12706.pdf", getAnnotationSelector("63R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must let checkboxes with the same name behave like radio buttons", async () => {
const selectors = [63, 70, 79].map(n => getAnnotationSelector(`${n}R`));
await Promise.all(
pages.map(async ([browserName, page]) => {
for (const selector of selectors) {
await page.click(selector);
await page.waitForFunction(
`document.querySelector('${selector} > :first-child').checked`
);
for (const otherSelector of selectors) {
const checked = await page.$eval(
`${otherSelector} > :first-child`,
el => el.checked
);
expect(checked)
.withContext(`In ${browserName}`)
.toBe(selector === otherSelector);
}
}
})
);
});
});
describe("issue15597.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue15597.pdf", getAnnotationSelector("7R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must check the checkbox", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selector = getAnnotationSelector("7R");
await page.click(selector);
await page.waitForFunction(
`document.querySelector('${selector} > :first-child').checked`
);
expect(true).withContext(`In ${browserName}`).toEqual(true);
})
);
});
});
describe("bug1847733.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug1847733.pdf", getAnnotationSelector("18R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must check the checkbox", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selectors = [18, 30, 42, 54].map(id =>
getAnnotationSelector(`${id}R`)
);
for (const selector of selectors) {
await page.click(selector);
await page.waitForFunction(
`document.querySelector('${selector} > :first-child').checked`
);
}
})
);
});
});
});
describe("Text widget", () => {
describe("issue13271.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue13271.pdf", getAnnotationSelector("24R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must update all the fields with the same value", async () => {
const base = "hello world";
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.type(getSelector("25R"), base);
await page.waitForFunction(`${getQuerySelector("24R")}.value !== ""`);
await page.waitForFunction(`${getQuerySelector("26R")}.value !== ""`);
let text = await page.$eval(getSelector("24R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(base);
text = await page.$eval(getSelector("26R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(base);
})
);
});
});
describe("issue16473.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue16473.pdf", getAnnotationSelector("22R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must reset a formatted value after a change", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.type(getSelector("22R"), "a");
await page.keyboard.press("Tab");
await page.waitForFunction(
`${getQuerySelector("22R")}.value !== "Hello world"`
);
const text = await page.$eval(getSelector("22R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("aHello World");
})
);
});
});
});
describe("Link annotations with internal destinations", () => {
describe("bug1708041.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1708041.pdf",
".page[data-page-number='1'] .annotationLayer"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must click on a link and check if it navigates to the correct page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const pageOneSelector = ".page[data-page-number='1']";
const linkSelector = `${pageOneSelector} #pdfjs_internal_id_42R`;
await page.waitForSelector(linkSelector);
const linkTitle = await page.$eval(linkSelector, el => el.title);
expect(linkTitle)
.withContext(`In ${browserName}`)
.toEqual("Go to the last page");
await page.click(linkSelector);
const pageSixTextLayerSelector =
".page[data-page-number='6'] .textLayer";
await page.waitForSelector(pageSixTextLayerSelector, {
visible: true,
});
await page.waitForFunction(
sel => {
const textLayer = document.querySelector(sel);
return document.activeElement === textLayer;
},
{},
pageSixTextLayerSelector
);
})
);
});
});
});
describe("Annotation and storage", () => {
describe("issue14023.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue14023.pdf", getAnnotationSelector("64R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must let checkboxes with the same name behave like radio buttons", async () => {
const text1 = "hello world!";
const text2 = "!dlrow olleh";
await Promise.all(
pages.map(async ([browserName, page]) => {
// Text field.
await page.type(getSelector("64R"), text1);
// Checkbox.
await page.click(getAnnotationSelector("65R"));
// Radio.
await page.click(getAnnotationSelector("67R"));
for (const [pageNumber, textId, checkId, radio1Id, radio2Id] of [
[2, "18R", "19R", "21R", "20R"],
[5, "23R", "24R", "22R", "25R"],
]) {
await page.evaluate(n => {
window.document
.querySelectorAll(`[data-page-number="${n}"][class="page"]`)[0]
.scrollIntoView();
}, pageNumber);
// Need to wait to have a displayed text input.
await page.waitForSelector(getSelector(textId), {
timeout: 0,
});
const text = await page.$eval(getSelector(textId), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(text1);
let checked = await page.$eval(
getSelector(checkId),
el => el.checked
);
expect(checked).toEqual(true);
checked = await page.$eval(getSelector(radio1Id), el => el.checked);
expect(checked).toEqual(false);
checked = await page.$eval(getSelector(radio2Id), el => el.checked);
expect(checked).toEqual(false);
}
// Change data on page 5 and check that other pages changed.
// Text field.
await page.type(getSelector("23R"), text2);
// Checkbox.
await page.click(getAnnotationSelector("24R"));
// Radio.
await page.click(getAnnotationSelector("25R"));
for (const [pageNumber, textId, checkId, radio1Id, radio2Id] of [
[1, "64R", "65R", "67R", "68R"],
[2, "18R", "19R", "21R", "20R"],
]) {
await page.evaluate(n => {
window.document
.querySelectorAll(`[data-page-number="${n}"][class="page"]`)[0]
.scrollIntoView();
}, pageNumber);
// Need to wait to have a displayed text input.
await page.waitForSelector(getSelector(textId), {
timeout: 0,
});
const text = await page.$eval(getSelector(textId), el => el.value);
expect(text)
.withContext(`In ${browserName}`)
.toEqual(text2 + text1);
let checked = await page.$eval(
getSelector(checkId),
el => el.checked
);
expect(checked).toEqual(false);
checked = await page.$eval(getSelector(radio1Id), el => el.checked);
expect(checked).toEqual(false);
checked = await page.$eval(getSelector(radio2Id), el => el.checked);
expect(checked).toEqual(false);
}
})
);
});
});
});
describe("ResetForm action", () => {
describe("resetform.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("resetform.pdf", getAnnotationSelector("63R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must reset all fields", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const base = "hello world";
for (let i = 63; i <= 67; i++) {
await page.type(getSelector(`${i}R`), base);
}
const selectors = [69, 71, 75].map(n =>
getAnnotationSelector(`${n}R`)
);
for (const selector of selectors) {
await page.click(selector);
}
await page.select(getSelector("78R"), "b");
await page.select(getSelector("81R"), "f");
await page.click(getAnnotationSelector("82R"));
await page.waitForFunction(`${getQuerySelector("63R")}.value === ""`);
for (let i = 63; i <= 68; i++) {
const text = await page.$eval(getSelector(`${i}R`), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("");
}
const ids = [69, 71, 72, 73, 74, 75, 76, 77];
for (const id of ids) {
const checked = await page.$eval(
getSelector(`${id}R`),
el => el.checked
);
expect(checked).withContext(`In ${browserName}`).toEqual(false);
}
let selected = await page.$eval(
`${getSelector("78R")} [value="a"]`,
el => el.selected
);
expect(selected).withContext(`In ${browserName}`).toEqual(true);
selected = await page.$eval(
`${getSelector("81R")} [value="d"]`,
el => el.selected
);
expect(selected).withContext(`In ${browserName}`).toEqual(true);
})
);
});
it("must reset some fields", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const base = "hello world";
for (let i = 63; i <= 68; i++) {
await page.type(getSelector(`${i}R`), base);
}
const selectors = [69, 71, 72, 73, 75].map(n =>
getAnnotationSelector(`${n}R`)
);
for (const selector of selectors) {
await page.click(selector);
}
await page.select(getSelector("78R"), "b");
await page.select(getSelector("81R"), "f");
await page.click(getAnnotationSelector("84R"));
await page.waitForFunction(`${getQuerySelector("63R")}.value === ""`);
for (let i = 63; i <= 68; i++) {
const expected = (i - 3) % 2 === 0 ? "" : base;
const text = await page.$eval(getSelector(`${i}R`), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual(expected);
}
let ids = [69, 72, 73, 74, 76, 77];
for (const id of ids) {
const checked = await page.$eval(
getSelector(`${id}R`),
el => el.checked
);
expect(checked)
.withContext(`In ${browserName + id}`)
.toEqual(false);
}
ids = [71, 75];
for (const id of ids) {
const checked = await page.$eval(
getSelector(`${id}R`),
el => el.checked
);
expect(checked).withContext(`In ${browserName}`).toEqual(true);
}
let selected = await page.$eval(
`${getSelector("78R")} [value="a"]`,
el => el.selected
);
expect(selected).withContext(`In ${browserName}`).toEqual(true);
selected = await page.$eval(
`${getSelector("81R")} [value="f"]`,
el => el.selected
);
expect(selected).withContext(`In ${browserName}`).toEqual(true);
})
);
});
});
describe("FreeText widget", () => {
describe("issue14438.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"issue14438.pdf",
getAnnotationSelector("10R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the FreeText annotation has a popup", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selector = getAnnotationSelector("10R");
await page.click(selector);
await page.waitForFunction(
`document.querySelector('${selector}').hidden === false`
);
})
);
});
});
});
describe("Ink widget and its popup after editing", () => {
describe("annotation-caret-ink.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"annotation-caret-ink.pdf",
getAnnotationSelector("25R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the Ink annotation has a popup", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selector = getAnnotationSelector("25R");
await page.waitForFunction(
`document.querySelector('${selector}').hidden === false`
);
await page.click("#editorFreeText");
await page.waitForFunction(
`document.querySelector('${selector}').hidden === true`
);
await page.click("#editorFreeText");
await page.waitForFunction(
`document.querySelector('${selector}').hidden === false`
);
})
);
});
});
});
describe("Don't use AP when /NeedAppearances is true", () => {
describe("bug1844583.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1844583.pdf",
getAnnotationSelector("8R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check the content of the text field", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(getSelector("8R"), el => el.value);
expect(text)
.withContext(`In ${browserName}`)
.toEqual("Hello World");
})
);
});
});
});
describe("Toggle popup with keyboard", () => {
describe("tagged_stamp.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tagged_stamp.pdf",
getAnnotationSelector("20R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the popup has the correct visibility", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const selector = getAnnotationSelector("21R");
let hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(true);
await page.focus(getAnnotationSelector("20R"));
await page.keyboard.press("Enter");
await page.waitForFunction(
`document.querySelector('${selector}').hidden !== true`
);
hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(false);
await page.keyboard.press("Enter");
await page.waitForFunction(
`document.querySelector('${selector}').hidden !== false`
);
hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(true);
await page.keyboard.press("Enter");
await page.waitForFunction(
`document.querySelector('${selector}').hidden !== true`
);
hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(false);
await page.keyboard.press("Escape");
await page.waitForFunction(
`document.querySelector('${selector}').hidden !== false`
);
hidden = await page.$eval(selector, el => el.hidden);
expect(hidden).withContext(`In ${browserName}`).toEqual(true);
})
);
});
});
});
describe("Annotation with empty popup and aria", () => {
describe("issue14438.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"highlights.pdf",
getAnnotationSelector("693R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the highlight annotation has no popup and no aria-haspopup attribute", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const highlightSelector = getAnnotationSelector("693R");
const popupSelector = getAnnotationSelector("694R");
await page.waitForFunction(
// No aria-haspopup attribute,
`document.querySelector('${highlightSelector}').ariaHasPopup === null ` +
// and no popup.
`&& document.querySelector('${popupSelector}') === null`
);
})
);
});
});
});
describe("Rotated annotation and its clickable area", () => {
describe("rotated_ink.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"rotated_ink.pdf",
getAnnotationSelector("18R")
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the clickable area has been rotated", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const rect = await getRect(page, getAnnotationSelector("18R"));
const promisePopup = page.waitForSelector(
getAnnotationSelector("19R"),
{ visible: true }
);
await page.mouse.move(
rect.x + rect.width * 0.1,
rect.y + rect.height * 0.9
);
await promisePopup;
})
);
});
});
});
describe("Text under some annotations", () => {
describe("bug1885505.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1885505.pdf",
":is(" +
[56, 58, 60, 65]
.map(id => getAnnotationSelector(`${id}R`))
.join(", ") +
")"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the text under a highlight annotation exist in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(
`${getAnnotationSelector("56R")} mark`,
el => el.textContent
);
expect(text).withContext(`In ${browserName}`).toEqual("Languages");
})
);
});
it("must check that the text under an underline annotation exist in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(
`${getAnnotationSelector("58R")} u`,
el => el.textContent
);
expect(text).withContext(`In ${browserName}`).toEqual("machine");
})
);
});
it("must check that the text under a squiggly annotation exist in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(
`${getAnnotationSelector("60R")} u`,
el => el.textContent
);
expect(text).withContext(`In ${browserName}`)
.toEqual(`paths through nested loops. We have implemented
a dynamic compiler for JavaScript based on our`);
})
);
});
it("must check that the text under a strikeout annotation exist in the DOM", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(
`${getAnnotationSelector("65R")} s`,
el => el.textContent
);
expect(text)
.withContext(`In ${browserName}`)
.toEqual("Experimentation,");
})
);
});
});
});
describe("Annotation without popup and enableComment set to true", () => {
describe("annotation-text-without-popup.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"annotation-text-without-popup.pdf",
getAnnotationSelector("4R"),
"page-fit",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the popup is shown", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const rect = await getRect(page, getAnnotationSelector("4R"));
// Hover the annotation, the popup should be visible.
let promisePopup = page.waitForSelector("#commentPopup", {
visible: true,
});
await page.mouse.move(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
await promisePopup;
// Move the mouse away, the popup should be hidden.
promisePopup = page.waitForSelector("#commentPopup", {
visible: false,
});
await page.mouse.move(
rect.x - rect.width / 2,
rect.y - rect.height / 2
);
await promisePopup;
// Click the annotation, the popup should be visible.
promisePopup = page.waitForSelector("#commentPopup", {
visible: true,
});
await page.mouse.click(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
await promisePopup;
// Click again, the popup should be hidden.
promisePopup = page.waitForSelector("#commentPopup", {
visible: false,
});
await page.mouse.click(
rect.x + rect.width / 2,
rect.y + rect.height / 2
);
await promisePopup;
})
);
});
});
});
describe("Annotation order in the DOM", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"comments.pdf",
".page[data-page-number='1'] .annotationLayer #pdfjs_internal_id_661R"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that annotations are in the visual order", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const sectionIds = await page.evaluate(() =>
[
...document.querySelectorAll(
".page[data-page-number='1'] .annotationLayer > section:not(.popupAnnotation)"
),
].map(el => el.id.split("_").pop())
);
expect(sectionIds)
.withContext(`In ${browserName}`)
.toEqual([
"612R",
"693R",
"687R",
"690R",
"713R",
"673R",
"613R",
"680R",
"661R",
]);
})
);
});
});
describe("bug 2026037", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("bug2026037.pdf", getAnnotationSelector("22R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must check that spaces in a choice option display value are preserved", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// The option's displayValue contains multiple consecutive spaces
// ("A B"). Browsers collapse spaces in textContent, so the
// fix stores the original in a "display-value" attribute and uses
// non-breaking spaces (\u00A0) in textContent.
const displayAttr = await page.$eval(
`${getSelector("22R")} option`,
el => el.getAttribute("display-value")
);
expect(displayAttr)
.withContext(`In ${browserName}`)
.toEqual("A B");
const textContent = await page.$eval(
`${getSelector("22R")} option`,
el => el.textContent
);
expect(textContent)
.withContext(`In ${browserName}`)
.toEqual("A\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0\u00A0B");
const exportValue = await page.$eval(
`${getSelector("22R")} option`,
el => el.value
);
expect(exportValue)
.withContext(`In ${browserName}`)
.toEqual("a b");
})
);
});
});
});

View File

@@ -0,0 +1,262 @@
/* Copyright 2025 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
awaitPromise,
closePages,
createPromise,
loadAndWait,
} from "./test_utils.mjs";
function waitForLinkAnnotations(page, pageNumber) {
return page.evaluateHandle(
number => [
new Promise(resolve => {
const { eventBus } = window.PDFViewerApplication;
eventBus.on("linkannotationsadded", function listener(e) {
if (number === undefined || e.pageNumber === number) {
resolve();
eventBus.off("linkannotationsadded", listener);
}
});
}),
],
pageNumber
);
}
function recordInitialLinkAnnotationsEvent(eventBus) {
globalThis.initialLinkAnnotationsEventFired = false;
eventBus.on(
"linkannotationsadded",
() => {
globalThis.initialLinkAnnotationsEventFired = true;
},
{ once: true }
);
}
function waitForInitialLinkAnnotations(page) {
return createPromise(page, resolve => {
if (globalThis.initialLinkAnnotationsEventFired) {
resolve();
return;
}
window.PDFViewerApplication.eventBus.on("linkannotationsadded", resolve, {
once: true,
});
});
}
describe("autolinker", function () {
describe("bug1019475_2.pdf", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1019475_2.pdf",
".annotationLayer",
null,
null,
{
enableAutoLinking: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must appropriately add link annotations when relevant", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
const url = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
expect(url[0])
.withContext(`In ${browserName}`)
.toEqual("http://www.mozilla.org/");
})
);
});
});
describe("bug1019475_1.pdf", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1019475_1.pdf",
".annotationLayer",
null,
null,
{
enableAutoLinking: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must not add links when unnecessary", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
const linkIds = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations =>
annotations.map(a => a.getAttribute("data-element-id"))
);
expect(linkIds.length).withContext(`In ${browserName}`).toEqual(3);
linkIds.forEach(id =>
expect(id)
.withContext(`In ${browserName}`)
.not.toContain("inferred_link_")
);
})
);
});
});
describe("pr19449.pdf", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait("pr19449.pdf", ".annotationLayer", null, null, {
docBaseUrl: "http://example.com",
enableAutoLinking: true,
});
});
afterEach(async () => {
await closePages(pages);
});
it("must not add links that overlap even if the URLs are different", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
const linkIds = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations =>
annotations.map(a => a.getAttribute("data-element-id"))
);
expect(linkIds.length).withContext(`In ${browserName}`).toEqual(1);
linkIds.forEach(id =>
expect(id)
.withContext(`In ${browserName}`)
.not.toContain("inferred_link_")
);
})
);
});
});
describe("PR 19470", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1019475_2.pdf",
".annotationLayer",
null,
null,
{
enableAutoLinking: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must not repeatedly add link annotations redundantly", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForLinkAnnotations(page);
let url = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
await page.evaluate(() =>
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 2,
})
);
await waitForLinkAnnotations(page);
url = await page.$$eval(
".annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
})
);
});
});
describe("when highlighting search results", function () {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"issue3115r.pdf",
".annotationLayer",
null,
{ eventBusSetup: recordInitialLinkAnnotationsEvent },
{ enableAutoLinking: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must find links that overlap with search results", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await awaitPromise(await waitForInitialLinkAnnotations(page));
const linkAnnotationsPromise = await waitForLinkAnnotations(page, 36);
// Search for "rich.edu"
await page.click("#viewFindButton");
await page.waitForSelector("#viewFindButton", { hidden: false });
await page.type("#findInput", "rich.edu");
await page.waitForSelector(".textLayer .highlight");
await awaitPromise(linkAnnotationsPromise);
const urls = await page.$$eval(
".page[data-page-number='36'] > .annotationLayer > .linkAnnotation > a",
annotations => annotations.map(a => a.href)
);
expect(urls)
.withContext(`In ${browserName}`)
.toContain(jasmine.stringContaining("rich.edu"));
})
);
});
});
});

View File

@@ -0,0 +1,94 @@
/* Copyright 2021 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
closePages,
getRect,
loadAndWait,
waitForSelectionChange,
} from "./test_utils.mjs";
describe("Caret browsing", () => {
describe("Selection", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must move the caret down and check the selection", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const spanRect = await getRect(
page,
`.page[data-page-number="1"] > .textLayer > span`
);
await page.mouse.click(
spanRect.x + 1,
spanRect.y + spanRect.height / 2,
{ count: 2 }
);
await page.keyboard.down("Shift");
for (let i = 0; i < 6; i++) {
await page.keyboard.press("ArrowRight");
}
await page.keyboard.up("Shift");
await waitForSelectionChange(page, "Trace-based");
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
// The caret is just before Languages.
await waitForSelectionChange(
page,
"Trace-based Just-in-Time Type Specialization for Dynamic\n"
);
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
// The caret is just before Mike Shaver.
await waitForSelectionChange(
page,
"Trace-based Just-in-Time Type Specialization for Dynamic\nLanguages\nAndreas Gal+, Brendan Eich, "
);
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Shift");
// The caret is just before Languages.
await waitForSelectionChange(
page,
"Trace-based Just-in-Time Type Specialization for Dynamic\n"
);
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Shift");
// The caret is in the middle of Time.
await waitForSelectionChange(page, "Trace-based Just-in-Tim");
})
);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,181 @@
/* Copyright 2023 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
closePages,
copy,
kbSelectAll,
loadAndWait,
mockClipboard,
waitForEvent,
} from "./test_utils.mjs";
const selectAll = async page => {
await waitForEvent({
page,
eventName: "selectionchange",
action: () => kbSelectAll(page),
});
await page.waitForFunction(() => {
const selection = document.getSelection();
const hiddenCopyElement = document.getElementById("hiddenCopyElement");
return selection.containsNode(hiddenCopyElement);
});
};
describe("Copy and paste", () => {
describe("all text", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", "#hiddenCopyElement", 100);
await mockClipboard(pages);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that we've all the contents on copy/paste", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(
".page[data-page-number='1'] .textLayer .endOfContent"
);
await selectAll(page);
await copy(page);
await page.waitForFunction(
`document.querySelector('#viewerContainer').style.cursor !== "wait"`
);
await page.waitForFunction(
async () =>
!!(await navigator.clipboard.readText())?.includes(
"Dynamic languages such as JavaScript"
)
);
const text = await page.evaluate(() =>
navigator.clipboard.readText()
);
expect(
text.includes("This section provides an overview of our system")
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"are represented by function calls. This makes the LIR used by"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes("When compiling loops, we consult the oracle before")
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(text.includes("Nested Trace Tree Formation"))
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"An important detail is that the call to the inner trace"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(text.includes("When trace recording is completed, nanojit"))
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"SpiderMonkey, like many VMs, needs to preempt the user program"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"Using similar computations, we find that trace recording takes"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"specialization algorithm. We also described our trace compiler"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(
text.includes(
"dynamic optimization system. In Proceedings of the ACM SIGPLAN"
)
)
.withContext(`In ${browserName}`)
.toEqual(true);
})
);
});
});
describe("Copy/paste and ligatures", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"copy_paste_ligatures.pdf",
"#hiddenCopyElement",
100
);
await mockClipboard(pages);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the ligatures have been removed when the text has been copied", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector(
".page[data-page-number='1'] .textLayer .endOfContent"
);
await selectAll(page);
await copy(page);
await page.waitForFunction(
`document.querySelector('#viewerContainer').style.cursor !== "wait"`
);
await page.waitForFunction(
async () => !!(await navigator.clipboard.readText())
);
const text = await page.evaluate(() =>
navigator.clipboard.readText()
);
expect(text)
.withContext(`In ${browserName}`)
.toEqual("abcdeffffiflffifflſtstghijklmno");
})
);
});
});
});

View File

@@ -0,0 +1,134 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
closePages,
getRect,
loadAndWait,
waitForSelectionChange,
} from "./test_utils.mjs";
async function enableSelectTool(page) {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { hidden: false });
await page.click("#cursorSelectTool");
await page.waitForFunction(
"window.PDFViewerApplication.pdfCursorTools.activeTool === 0"
);
}
async function enableHandTool(page) {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { hidden: false });
await page.click("#cursorHandTool");
await page.waitForFunction(
"window.PDFViewerApplication.pdfCursorTools.activeTool === 1"
);
}
describe("Cursor tools", () => {
describe("Text selection", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
100
);
});
afterEach(async () => {
await closePages(pages);
});
it("check that text selection works", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await enableSelectTool(page);
const spanRect = await getRect(
page,
`.page[data-page-number="1"] > .textLayer > span`
);
const x = spanRect.x + 1,
y = spanRect.y + spanRect.height / 2;
await page.mouse.click(x, y, { count: 3 });
await waitForSelectionChange(
page,
"Trace-based Just-in-Time Type Specialization for Dynamic"
);
// Remove the selection.
await page.mouse.click(x, y);
await waitForSelectionChange(page, "");
})
);
});
});
describe("Hand tool", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
100
);
});
afterEach(async () => {
await closePages(pages);
});
it("check that hand tool scrolling works", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await enableHandTool(page);
const pageHeight = await page.evaluate(
() =>
document.querySelector(`.page[data-page-number="1"]`).offsetHeight
);
const steps = 10,
delta = Math.floor(pageHeight / steps);
const spanRect = await getRect(
page,
`.page[data-page-number="1"] > .textLayer > span`
);
const x = spanRect.x + 1,
y = spanRect.y + spanRect.height / 2;
for (let i = 0; i < steps; i++) {
await page.mouse.move(x, y);
await page.mouse.down();
await page.mouse.move(x, y - delta);
await page.mouse.up();
}
// Ensure that the second page is visible.
await page.waitForFunction("window.PDFViewerApplication.page === 2");
// Finally, disable the hand tool.
await enableSelectTool(page);
})
);
});
});
});

View File

@@ -0,0 +1,429 @@
/* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs";
import fs from "fs/promises";
const FIELDS = [
"fileName",
"fileSize",
"title",
"author",
"subject",
"keywords",
"creationDate",
"modificationDate",
"creator",
"producer",
"version",
"pageCount",
"pageSize",
"linearized",
];
async function openDocumentProperties(page) {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { hidden: false });
await page.click("#documentProperties");
await page.waitForSelector("#documentPropertiesDialog", {
hidden: false,
});
}
async function closeDocumentProperties(page) {
await page.click("#documentPropertiesClose");
await page.waitForSelector("#documentPropertiesDialog", {
hidden: true,
});
}
async function checkFieldProperties(page, expectedProps) {
await page.waitForFunction(
`document.getElementById("fileSizeField").textContent !== "-"`
);
const promises = [];
for (const name of FIELDS) {
promises.push(
page.evaluate(
n => [n, document.getElementById(`${n}Field`).textContent],
name
)
);
}
const props = Object.fromEntries(await Promise.all(promises));
expect(props).toEqual(expectedProps);
}
function getFieldDataLastUpdated(page) {
return page.evaluate(
() =>
document.getElementById("documentPropertiesDialog").dataset
.fieldDataLastUpdated
);
}
describe("PDFDocumentProperties", () => {
describe("Document with both /Info and /Metadata", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "basicapi.pdf",
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
title: "Basic API Test",
author: "Brendan Dahl",
subject: "-",
keywords: "TCPDF",
creationDate: "4/10/12, 7:30:26 AM",
modificationDate: "4/10/12, 7:30:26 AM",
creator: "TCPDF",
producer: "TCPDF 5.9.133 (http://www.tcpdf.org)",
version: "1.7",
pageCount: "3",
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
linearized: "No",
});
await closeDocumentProperties(page);
})
);
});
});
describe("Document with approximately A4-sized page", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"arial_unicode_en_cidfont.pdf",
".textLayer .endOfContent"
);
});
afterEach(async () => {
await closePages(pages);
});
it("check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "arial_unicode_en_cidfont.pdf",
fileSize: `${FSI}15.4${PDI} KB (${FSI}15,779${PDI} bytes)`,
title: "-",
author: "Adil Allawi",
subject: "-",
keywords: "-",
creationDate: "7/10/11, 7:17:28 PM",
modificationDate: "-",
creator: "Writer",
producer: "NeoOffice 3.2 Beta",
version: "1.4",
pageCount: "1",
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
linearized: "No",
});
await closeDocumentProperties(page);
})
);
});
});
describe("Document without contentLength", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Open a binary PDF document, such that `contentLength` is undefined.
const base64 = await fs.readFile("./pdfs/clippath.pdf", {
encoding: "base64",
});
await page.evaluate(async b64 => {
await window.PDFViewerApplication.open({
data: Uint8Array.fromBase64(b64),
});
}, base64);
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "document.pdf",
fileSize: `${FSI}0.448${PDI} KB (${FSI}459${PDI} bytes)`,
title: "-",
author: "-",
subject: "-",
keywords: "-",
creationDate: "-",
modificationDate: "-",
creator: "-",
producer: "-",
version: "1.1",
pageCount: "1",
pageSize: `${FSI}2.78${PDI} × ${FSI}1.39${PDI} ${FSI}in${PDI} (${FSI}landscape${PDI})`,
linearized: "No",
});
await closeDocumentProperties(page);
})
);
});
});
describe("Document with multiple pages, and changed viewer page/rotation", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "basicapi.pdf",
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
title: "Basic API Test",
author: "Brendan Dahl",
subject: "-",
keywords: "TCPDF",
creationDate: "4/10/12, 7:30:26 AM",
modificationDate: "4/10/12, 7:30:26 AM",
creator: "TCPDF",
producer: "TCPDF 5.9.133 (http://www.tcpdf.org)",
version: "1.7",
pageCount: "3",
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
linearized: "No",
});
const fieldDataLastUpdated = await getFieldDataLastUpdated(page);
await closeDocumentProperties(page);
// Ensure that immediately re-opening the dialog doesn't cause
// the field-data to be fetched and parsed again.
await openDocumentProperties(page);
expect(await getFieldDataLastUpdated(page)).toEqual(
fieldDataLastUpdated
);
await closeDocumentProperties(page);
// Goto the second page, and rotate the document.
await page.click("#next");
await page.waitForFunction(
() => window.PDFViewerApplication.page === 2
);
await page.keyboard.press("r");
await page.waitForFunction(
() => window.PDFViewerApplication.pdfViewer.pagesRotation === 90
);
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "basicapi.pdf",
fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
title: "Basic API Test",
author: "Brendan Dahl",
subject: "-",
keywords: "TCPDF",
creationDate: "4/10/12, 7:30:26 AM",
modificationDate: "4/10/12, 7:30:26 AM",
creator: "TCPDF",
producer: "TCPDF 5.9.133 (http://www.tcpdf.org)",
version: "1.7",
pageCount: "3",
pageSize: `${FSI}11.69${PDI} × ${FSI}8.27${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}landscape${PDI})`,
linearized: "No",
});
await closeDocumentProperties(page);
})
);
});
});
describe("Document with different page sizes", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("sizes.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "sizes.pdf",
fileSize: `${FSI}13.4${PDI} KB (${FSI}13,739${PDI} bytes)`,
title: "-",
author: "Yury ",
subject: "-",
keywords: "-",
creationDate: "6/26/11, 1:26:03 PM",
modificationDate: "-",
creator: "Writer",
producer: "OpenOffice.org 3.3",
version: "1.4",
pageCount: "3",
pageSize: `${FSI}8.5${PDI} × ${FSI}11${PDI} ${FSI}in${PDI} (${FSI}Letter${PDI}, ${FSI}portrait${PDI})`,
linearized: "No",
});
await closeDocumentProperties(page);
// Goto the second page.
await page.click("#next");
await page.waitForFunction(
() => window.PDFViewerApplication.page === 2
);
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "sizes.pdf",
fileSize: `${FSI}13.4${PDI} KB (${FSI}13,739${PDI} bytes)`,
title: "-",
author: "Yury ",
subject: "-",
keywords: "-",
creationDate: "6/26/11, 1:26:03 PM",
modificationDate: "-",
creator: "Writer",
producer: "OpenOffice.org 3.3",
version: "1.4",
pageCount: "3",
pageSize: `${FSI}9.01${PDI} × ${FSI}4.49${PDI} ${FSI}in${PDI} (${FSI}landscape${PDI})`,
linearized: "No",
});
await closeDocumentProperties(page);
})
);
});
});
describe("Document with corrupt page", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"Pages-tree-refs.pdf",
".textLayer .endOfContent",
null,
null,
{ page: 2 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "Pages-tree-refs.pdf",
fileSize: `${FSI}1.07${PDI} KB (${FSI}1,098${PDI} bytes)`,
title: "-",
author: "-",
subject: "-",
keywords: "-",
creationDate: "-",
modificationDate: "-",
creator: "-",
producer: "-",
version: "1.7",
pageCount: "2",
pageSize: "-",
linearized: "No",
});
await closeDocumentProperties(page);
// Goto the first page (which is *not* corrupt).
await page.click("#previous");
await page.waitForFunction(
() => window.PDFViewerApplication.page === 1
);
await openDocumentProperties(page);
await checkFieldProperties(page, {
fileName: "Pages-tree-refs.pdf",
fileSize: `${FSI}1.07${PDI} KB (${FSI}1,098${PDI} bytes)`,
title: "-",
author: "-",
subject: "-",
keywords: "-",
creationDate: "-",
modificationDate: "-",
creator: "-",
producer: "-",
version: "1.7",
pageCount: "2",
pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
linearized: "No",
});
await closeDocumentProperties(page);
})
);
});
});
});

View File

@@ -0,0 +1,234 @@
/* Copyright 2021 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
closePages,
FSI,
loadAndWait,
PDI,
waitForTextToBe,
} from "./test_utils.mjs";
function fuzzyMatch(a, b, browserName, pixelFuzz = 3) {
expect(a)
.withContext(`In ${browserName}`)
.toBeLessThan(b + pixelFuzz);
expect(a)
.withContext(`In ${browserName}`)
.toBeGreaterThan(b - pixelFuzz);
}
describe("find bar", () => {
describe("highlight all", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("find_all.pdf", ".textLayer", 100);
});
afterEach(async () => {
await closePages(pages);
});
it("must highlight text in the right position", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Highlight all occurrences of the letter A (case insensitive).
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "a");
await page.click("#findHighlightAll + label");
await page.waitForSelector(".textLayer .highlight");
// The PDF file contains the text 'AB BA' in a monospace font on a
// single line. Check if the two occurrences of A are highlighted.
const highlights = await page.$$(".textLayer .highlight");
expect(highlights.length).withContext(`In ${browserName}`).toEqual(2);
// Normalize the highlight's height. The font data in the PDF sets the
// size of the glyphs (and therefore the size of the highlights), but
// the viewer applies extra padding to them. For the comparison we
// therefore use the unpadded, glyph-sized parent element's height.
const parentSpan = (await highlights[0].$$("xpath/.."))[0];
const parentBox = await parentSpan.boundingBox();
const firstA = await highlights[0].boundingBox();
const secondA = await highlights[1].boundingBox();
firstA.height = parentBox.height;
secondA.height = parentBox.height;
// Check if the vertical position of the highlights is correct. Both
// should be on a single line.
expect(firstA.y).withContext(`In ${browserName}`).toEqual(secondA.y);
// Check if the height of the two highlights is correct. Both should
// match the font size.
const fontSize = 26.66; // From the PDF.
fuzzyMatch(firstA.height, fontSize, browserName);
fuzzyMatch(secondA.height, fontSize, browserName);
// Check if the horizontal position of the highlights is correct. The
// second occurrence should be four glyph widths (three letters and
// one space) away from the first occurrence.
const pageDiv = await page.$(".page canvas");
const pageBox = await pageDiv.boundingBox();
const expectedFirstAX = 30; // From the PDF.
const glyphWidth = 15.98; // From the PDF.
fuzzyMatch(firstA.x, pageBox.x + expectedFirstAX, browserName);
fuzzyMatch(secondA.x, firstA.x + glyphWidth * 4, browserName);
})
);
});
});
describe("highlight all (XFA)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("xfa_imm5257e.pdf", ".xfaLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must search xfa correctly", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "preferences");
await page.waitForSelector("#findInput[data-status='']");
await page.waitForSelector(".xfaLayer .highlight");
await waitForTextToBe(
page,
"#findResultsCount",
`${FSI}1${PDI} of ${FSI}1${PDI} match`
);
const selectedElement = await page.waitForSelector(
".highlight.selected"
);
const selectedText = await selectedElement.evaluate(
el => el.textContent
);
expect(selectedText).toEqual("Preferences");
})
);
});
});
describe("issue19207.pdf", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue19207.pdf", ".textLayer", 200);
});
afterEach(async () => {
await closePages(pages);
});
it("must scroll to the search result text", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Search for "40"
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "40");
const highlight = await page.waitForSelector(".textLayer .highlight");
expect(await highlight.isIntersectingViewport()).toBeTrue();
})
);
});
});
describe("scrolls to the search result text for smaller viewports", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".textLayer", 100);
});
afterEach(async () => {
await closePages(pages);
});
it("must scroll to the search result text", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Set a smaller viewport to simulate a mobile device
await page.setViewport({ width: 350, height: 600 });
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "productivity");
const highlight = await page.waitForSelector(".textLayer .highlight");
expect(await highlight.isIntersectingViewport()).toBeTrue();
})
);
});
});
describe("Check that the search results are correctly visible in rotated PDFs (bug 2021392)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"hello_world_rotated.pdf",
".textLayer",
"page-fit"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must scroll each match into the viewport when navigating search results", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "hello");
await page.waitForSelector("#findInput[data-status='']");
for (let i = 0; i < 5; i++) {
if (i > 0) {
await page.click("#findNextButton");
await page.waitForSelector("#findInput[data-status='']");
}
// Verify we are on the expected match number.
await waitForTextToBe(
page,
"#findResultsCount",
`${FSI}${i + 1}${PDI} of ${FSI}5${PDI} matches`
);
// The selected highlight must be visible in the viewport.
const selected = await page.waitForSelector(
".textLayer .highlight.selected"
);
expect(await selected.isIntersectingViewport())
.withContext(`In ${browserName}, match ${i + 1}`)
.toBeTrue();
}
})
);
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,115 @@
/* Copyright 2020 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/* eslint-disable no-console */
import { TEST_PASSED, TEST_UNEXPECTED_FAIL } from "../color_utils.mjs";
import Jasmine from "jasmine";
async function runTests(results) {
const jasmine = new Jasmine();
jasmine.exitOnCompletion = false;
jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
jasmine.loadConfig({
random: true,
spec_dir: "integration",
spec_files: [
"accessibility_spec.mjs",
"annotation_spec.mjs",
"autolinker_spec.mjs",
"caret_browsing_spec.mjs",
"comment_spec.mjs",
"copy_paste_spec.mjs",
"cursor_tools_spec.mjs",
"document_properties_spec.mjs",
"find_spec.mjs",
"freetext_editor_spec.mjs",
"highlight_editor_spec.mjs",
"ink_editor_spec.mjs",
"presentation_mode_spec.mjs",
"reorganize_pages_spec.mjs",
"scripting_spec.mjs",
"signature_editor_spec.mjs",
"simple_viewer_spec.mjs",
"stamp_editor_spec.mjs",
"text_extractor_spec.mjs",
"text_field_spec.mjs",
"text_layer_spec.mjs",
"text_layer_images_spec.mjs",
"thumbnail_view_spec.mjs",
"viewer_spec.mjs",
],
});
function failureError(result) {
return result.failedExpectations
?.map(item => item.message)
.filter(Boolean)
.join(" ");
}
jasmine.addReporter({
jasmineDone(suiteInfo) {},
jasmineStarted(suiteInfo) {},
specDone(result) {
// Ignore excluded (fit/xit) or skipped (pending) tests.
if (["excluded", "pending"].includes(result.status)) {
return;
}
// Report on passed or failed tests.
++results.runs;
if (result.status === "passed") {
console.log(`${TEST_PASSED} | ${result.description}`);
} else {
++results.failures;
const error = failureError(result);
results.failureList?.push({
description: result.description,
error,
});
console.log(
`${TEST_UNEXPECTED_FAIL} | ${result.description}${error ? ` | ${error}` : ""}`
);
}
},
specStarted(result) {},
suiteDone(result) {
// Ignore excluded (fdescribe/xdescribe) or skipped (pending) suites.
if (["excluded", "pending"].includes(result.status)) {
return;
}
// Report on failed suites only (indicates problems in setup/teardown).
if (result.status === "failed") {
++results.failures;
const error = failureError(result);
results.failureList?.push({
description: result.description,
error,
});
console.log(
`${TEST_UNEXPECTED_FAIL} | ${result.description}${error ? ` | ${error}` : ""}`
);
}
},
suiteStarted(result) {},
});
return jasmine.execute();
}
export { runTests };

View File

@@ -0,0 +1,196 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
awaitPromise,
closePages,
createPromise,
loadAndWait,
waitForTimeout,
} from "./test_utils.mjs";
async function enterPresentationMode(page) {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { hidden: false });
const handlePresentationModeChanged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"presentationmodechanged",
resolve,
{ once: true }
);
});
await page.click("#presentationMode");
await awaitPromise(handlePresentationModeChanged);
// Check that presentation mode is active and that the toolbar is
// invisible; the latter differentiates between proper presentation
// mode and pressing F11 to only hide the browser's UI elements.
await page.waitForFunction(`document.fullscreenElement !== null`);
await page.waitForSelector("#viewerContainer.pdfPresentationMode", {
visible: true,
});
await page.waitForSelector("#toolbarContainer", { visible: false });
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
);
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentScaleValue === "page-fit"`
);
}
async function exitPresentationMode(page, browserName) {
// Note that in Chrome pressing Escape does not work to exit full screen mode
// in the Puppeteer scope, so there we exit full screen mode programmatically
// instead, which is equivalent to what happens if Escape is pressed.
const handlePresentationModeChanged = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"presentationmodechanged",
resolve,
{ once: true }
);
});
await (browserName === "chrome"
? page.evaluate(() => document.exitFullscreen())
: page.keyboard.press("Escape"));
await awaitPromise(handlePresentationModeChanged);
// Check that presentation mode is not active anymore and the toolbar
// is visible again.
await page.waitForFunction(`document.fullscreenElement === null`);
await page.waitForSelector("#viewerContainer:not(.pdfPresentationMode)", {
visible: true,
});
await page.waitForSelector("#toolbarContainer", { visible: true });
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
);
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentScaleValue !== "page-fit"`
);
}
describe("PDFPresentationMode", () => {
describe("Changing pages", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"basicapi.pdf",
".textLayer .endOfContent",
100
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that changing pages using arrow keys works in presentation mode", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await enterPresentationMode(page);
// Go to the next page.
await page.keyboard.press("ArrowDown");
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 2`
);
// Go to the previous page.
await page.keyboard.press("ArrowUp");
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
);
await exitPresentationMode(page, browserName);
})
);
});
it("must check that changing pages using mouse wheel works in presentation mode", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await enterPresentationMode(page);
// Go to the next page.
await page.mouse.wheel({ deltaY: 100 });
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 2`
);
// Wait until the viewer accepts a new mouse scroll event; see
// `MOUSE_SCROLL_COOLDOWN_TIME` in `web/pdf_presentation_mode.js`.
// eslint-disable-next-line no-restricted-syntax
await waitForTimeout(50);
// Go to the previous page.
await page.mouse.wheel({ deltaY: -100 });
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
);
await exitPresentationMode(page, browserName);
})
);
});
it("must check that changing pages using mouse click works in presentation mode", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await enterPresentationMode(page);
// Go to the next page.
await page.click(".page[data-page-number='1']");
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 2`
);
// Go to the previous page.
await page.keyboard.down("Shift");
await page.click(".page[data-page-number='2']");
await page.keyboard.up("Shift");
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
);
await exitPresentationMode(page, browserName);
})
);
});
it("check that clicking on internal links work in presentation mode", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await enterPresentationMode(page);
// Go to the last page.
await page.click("#pdfjs_internal_id_12R");
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 3`
);
// Go to the first page.
await page.keyboard.press("Home");
await page.waitForFunction(
`window.PDFViewerApplication.pdfViewer.currentPageNumber === 1`
);
await exitPresentationMode(page, browserName);
})
);
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,774 @@
/* Copyright 2025 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
awaitPromise,
closePages,
copy,
FSI,
getEditorSelector,
getRect,
loadAndWait,
paste,
PDI,
switchToEditor,
waitForPointerUp,
waitForTimeout,
} from "./test_utils.mjs";
import fs from "fs";
import path from "path";
import { PNG } from "pngjs";
const __dirname = import.meta.dirname;
const switchToSignature = switchToEditor.bind(null, "Signature");
describe("Signature Editor", () => {
const descriptionInputSelector = "#addSignatureDescription > input";
const addButtonSelector = "#addSignatureAddButton";
describe("Basic operations", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the editor has been removed when the dialog is cancelled", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
// An invisible editor is created but invisible.
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: false });
await page.click("#addSignatureCancelButton");
// The editor should have been removed.
await page.waitForSelector(`:not(${editorSelector})`);
})
);
});
it("must check that the basic and common elements are working as expected", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.waitForSelector(
"#addSignatureTypeButton[aria-selected=true]"
);
await page.click("#addSignatureTypeInput");
await page.waitForSelector(
"#addSignatureSaveContainer > input:disabled"
);
let description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("");
await page.waitForSelector(`${addButtonSelector}:disabled`);
await page.waitForSelector("#addSignatureDescInput:disabled");
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.waitForSelector("#addSignatureDescInput:not(:disabled)");
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
// The description has been filled in automatically.
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("PDF.js");
// Clear the description.
await page.click("#addSignatureDescription > button");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
// Clear the text for the signature.
await page.click("#clearSignatureButton");
await page.waitForFunction(
`document.querySelector("#addSignatureTypeInput").value === ""`
);
// The save button should be disabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:disabled"
);
await page.waitForSelector(`${addButtonSelector}:disabled`);
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
// Clearing the signature type should clear the description.
await page.click("#clearSignatureButton");
await page.waitForFunction(
`document.querySelector("#addSignatureTypeInput").value === ""`
);
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
// Add a signature and change the description.
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
await page.click("#addSignatureDescription > button");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
await page.type(descriptionInputSelector, "Hello World");
await page.type("#addSignatureTypeInput", "Hello");
// The description mustn't be changed.
// eslint-disable-next-line no-restricted-syntax
await waitForTimeout(100);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("Hello World");
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_0"]`,
{ visible: true }
);
await page.waitForFunction(
`document.getElementById("viewer-alert").textContent === "Signature added"`
);
// Check the tooltip.
await page.waitForSelector(
`.altText.editDescription[title="Hello World"]`
);
// Check the aria description.
await page.waitForSelector(
`${editorSelector}[aria-description="Signature editor: ${FSI}Hello World${PDI}"]`
);
// Edit the description.
await page.click(`.altText.editDescription`);
await page.waitForSelector("#editSignatureDescriptionDialog", {
visible: true,
});
await page.waitForSelector("#editSignatureUpdateButton:disabled");
await page.waitForSelector(
`#editSignatureDescriptionDialog svg[aria-label="Hello World"]`
);
const editDescriptionInput = "#editSignatureDescription > input";
description = await page.$eval(editDescriptionInput, el => el.value);
expect(description).withContext(browserName).toEqual("Hello World");
await page.click("#editSignatureDescription > button");
await page.waitForFunction(
`document.querySelector("${editDescriptionInput}").value === ""`
);
await page.waitForSelector(
"#editSignatureUpdateButton:not(:disabled)"
);
await page.type(editDescriptionInput, "Hello PDF.js World");
await page.waitForSelector(
"#editSignatureUpdateButton:not(:disabled)"
);
await page.click("#editSignatureUpdateButton");
// Check the tooltip.
await page.waitForSelector(
`.altText.editDescription[title="Hello PDF.js World"]`
);
})
);
});
it("must check drawing with the mouse", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureDrawButton");
const drawSelector = "#addSignatureDraw";
await page.waitForSelector(drawSelector, { visible: true });
let description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("");
await page.waitForSelector(`${addButtonSelector}:disabled`);
const { x, y, width, height } = await getRect(page, drawSelector);
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(x + 0.1 * width, y + 0.1 * height);
await page.mouse.down();
await page.mouse.move(x + 0.3 * width, y + 0.3 * height);
await page.mouse.up();
await awaitPromise(clickHandle);
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
// The description has been filled in automatically.
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("Signature");
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
await page.waitForSelector(
".canvasWrapper > svg use[href='#path_0']"
);
})
);
});
it("must check adding an image", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureImageButton");
await page.waitForSelector("#addSignatureImagePlaceholder", {
visible: true,
});
let description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("");
await page.waitForSelector(`${addButtonSelector}:disabled`);
const input = await page.$("#addSignatureFilePicker");
await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}`
);
await page.waitForSelector(`#addSignatureImage > path:not([d=""])`);
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
// The description has been filled in automatically.
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description)
.withContext(browserName)
.toEqual("firefox_logo.png");
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
await page.waitForSelector(
".canvasWrapper > svg use[href='#path_0']"
);
})
);
});
it("must check copy and paste", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.type("#addSignatureTypeInput", "Hello");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.click("#addSignatureAddButton");
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
const originalRect = await getRect(page, editorSelector);
const originalDescription = await page.$eval(
`${editorSelector} .altText.editDescription`,
el => el.title
);
const originalL10nParameter = await page.$eval(editorSelector, el =>
el.getAttribute("data-l10n-args")
);
await copy(page);
await paste(page);
const pastedEditorSelector = getEditorSelector(1);
await page.waitForSelector(pastedEditorSelector, { visible: true });
const pastedRect = await getRect(page, pastedEditorSelector);
const pastedDescription = await page.$eval(
`${pastedEditorSelector} .altText.editDescription`,
el => el.title
);
const pastedL10nParameter = await page.$eval(pastedEditorSelector, el =>
el.getAttribute("data-l10n-args")
);
expect(pastedRect)
.withContext(`In ${browserName}`)
.not.toEqual(originalRect);
expect(pastedDescription)
.withContext(`In ${browserName}`)
.toEqual(originalDescription);
expect(pastedL10nParameter)
.withContext(`In ${browserName}`)
.toEqual(originalL10nParameter);
}
});
});
describe("Bug 1948741", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the editor isn't too large", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.type(
"#addSignatureTypeInput",
"[18:50:03] asset pdf.scripting.mjs 105 KiB [emitted] [javascript module] (name: main)"
);
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.click("#addSignatureAddButton");
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_0"]`,
{ visible: true }
);
const { width } = await getRect(page, editorSelector);
const { width: pageWidth } = await getRect(page, ".page");
expect(width).toBeLessThanOrEqual(pageWidth);
})
);
});
});
describe("Bug 1949201", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the error panel is correctly removed", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureImageButton");
await page.waitForSelector("#addSignatureImagePlaceholder", {
visible: true,
});
const input = await page.$("#addSignatureFilePicker");
await input.uploadFile(
`${path.join(__dirname, "./signature_editor_spec.mjs")}`
);
await page.waitForSelector("#addSignatureError", { visible: true });
await page.click("#addSignatureErrorCloseButton");
await page.waitForSelector("#addSignatureError", { visible: false });
await input.uploadFile(
`${path.join(__dirname, "./stamp_editor_spec.mjs")}`
);
await page.waitForSelector("#addSignatureError", { visible: true });
await page.click("#addSignatureTypeButton");
await page.waitForSelector(
"#addSignatureTypeButton[aria-selected=true]"
);
await page.waitForSelector("#addSignatureError", { visible: false });
await page.click("#addSignatureCancelButton");
})
);
});
});
describe("viewerCssTheme (light)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"empty.pdf",
".annotationEditorLayer",
null,
null,
{ viewerCssTheme: "1" }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the signature has the correct color with the light theme", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
const colorTheme = await page.evaluate(() => {
const html = document.querySelector("html");
const style = getComputedStyle(html);
return style.getPropertyValue("color-scheme");
});
expect(colorTheme).toEqual("light");
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.type("#addSignatureTypeInput", "Should be black.");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.click("#addSignatureAddButton");
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_0"]`,
{ visible: true }
);
const color = await page.evaluate(() => {
const use = document.querySelector(
`.canvasWrapper > svg use[href="#path_0"]`
);
return use.parentNode.getAttribute("fill");
});
expect(color).toEqual("#000000");
})
);
});
});
describe("viewerCssTheme (dark)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"empty.pdf",
".annotationEditorLayer",
null,
null,
{ viewerCssTheme: "2" }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the signature has the correct color with the dark theme", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
const colorTheme = await page.evaluate(() => {
const html = document.querySelector("html");
const style = getComputedStyle(html);
return style.getPropertyValue("color-scheme");
});
expect(colorTheme).toEqual("dark");
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.type("#addSignatureTypeInput", "Should be black.");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.click("#addSignatureAddButton");
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_0"]`,
{ visible: true }
);
const color = await page.evaluate(() => {
const use = document.querySelector(
`.canvasWrapper > svg use[href="#path_0"]`
);
return use.parentNode.getAttribute("fill");
});
expect(color).toEqual("#000000");
})
);
});
});
describe("Check the aspect ratio (bug 1962819)", () => {
let pages, contentWidth, contentHeight;
function getContentAspectRatio(png) {
const { width, height } = png;
const buffer = new Uint32Array(png.data.buffer);
let x0 = width;
let y0 = height;
let x1 = 0;
let y1 = 0;
for (let i = 0; i < height; i++) {
for (let j = 0; j < width; j++) {
if (buffer[width * i + j] !== 0) {
x0 = Math.min(x0, j);
y0 = Math.min(y0, i);
x1 = Math.max(x1, j);
y1 = Math.max(y1, i);
}
}
}
contentWidth = x1 - x0;
contentHeight = y1 - y0;
}
beforeAll(() => {
const data = fs.readFileSync(
path.join(__dirname, "../images/samplesignature.png")
);
const png = PNG.sync.read(data);
getContentAspectRatio(png);
});
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the signature has the correct aspect ratio", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureImageButton");
await page.waitForSelector("#addSignatureImagePlaceholder", {
visible: true,
});
await page.waitForSelector(`${addButtonSelector}:disabled`);
const input = await page.$("#addSignatureFilePicker");
await input.uploadFile(
`${path.join(__dirname, "../images/samplesignature.png")}`
);
await page.waitForSelector(`#addSignatureImage > path:not([d=""])`);
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
const { width, height } = await getRect(
page,
".canvasWrapper > svg use[href='#path_0']"
);
expect(Math.abs(contentWidth / width - contentHeight / height))
.withContext(
`In ${browserName} (${contentWidth}x${contentHeight} vs ${width}x${height})`
)
.toBeLessThan(0.25);
})
);
});
});
describe("Bug 1974257", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the signature save checkbox is disabled if storage is full", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
for (let i = 0; i < 6; i++) {
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureTypeInput");
await page.type("#addSignatureTypeInput", `PDF.js ${i}`);
if (i === 5) {
await page.waitForSelector(
"#addSignatureSaveCheckbox:not(checked)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:disabled");
} else {
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
await page.waitForSelector(
"#addSignatureSaveCheckbox:not(:disabled)"
);
}
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
}
})
);
});
});
describe("Bug 1975719", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that an error is displayed with a monochrome image", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.click("#addSignatureImageButton");
await page.waitForSelector("#addSignatureImagePlaceholder", {
visible: true,
});
const input = await page.$("#addSignatureFilePicker");
await input.uploadFile(
`${path.join(__dirname, "../images/red.png")}`
);
await page.waitForSelector("#addSignatureError", { visible: true });
await page.waitForSelector(
"#addSignatureErrorTitle[data-l10n-id='pdfjs-editor-add-signature-image-no-data-error-title']"
);
await page.waitForSelector(
"#addSignatureErrorDescription[data-l10n-id='pdfjs-editor-add-signature-image-no-data-error-description']"
);
await page.click("#addSignatureErrorCloseButton");
await page.waitForSelector("#addSignatureError", { visible: false });
})
);
});
});
});

View File

@@ -0,0 +1,107 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
// Integration tests for the simple viewer (test/components/).
function getSelector(id) {
return `[data-element-id="${id}"]`;
}
function getAnnotationSelector(id) {
return `[data-annotation-id="${id}"]`;
}
describe("Simple viewer", () => {
describe("TextLayerBuilder without abortSignal", () => {
let pages;
beforeEach(async () => {
const origin = new URL(global.integrationBaseUrl).origin;
pages = await Promise.all(
global.integrationSessions.map(async session => {
const page = await session.browser.newPage();
await page.goto(
`${origin}/test/components/simple-viewer.html` +
`?file=/test/pdfs/tracemonkey.pdf`
);
await page.bringToFront();
await page.waitForSelector(
"[data-page-number='1'] .textLayer .endOfContent"
);
await page.waitForSelector(
"[data-page-number='2'] .textLayer .endOfContent"
);
return [session.name, page];
})
);
});
afterEach(async () => {
await Promise.all(pages.map(([, page]) => page.close()));
});
it("must produce text spans in the text layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const count = await page.evaluate(
() => document.querySelectorAll(".textLayer span").length
);
expect(count).withContext(`In ${browserName}`).toBeGreaterThan(0);
})
);
});
});
describe("Scripting support with evaljs.pdf", () => {
let pages;
beforeEach(async () => {
const origin = new URL(global.integrationBaseUrl).origin;
pages = await Promise.all(
global.integrationSessions.map(async session => {
const page = await session.browser.newPage();
await page.goto(
`${origin}/test/components/simple-viewer.html` +
`?file=/test/pdfs/evaljs.pdf`
);
await page.bringToFront();
await page.waitForSelector(getSelector("55R"));
await page.waitForFunction(
"window.pdfScriptingManager?.ready === true"
);
return [session.name, page];
})
);
});
afterEach(async () => {
await Promise.all(pages.map(([, page]) => page.close()));
});
it("must evaluate JavaScript entered in the input field", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.type(getSelector("55R"), "1 + 1");
await page.click(getAnnotationSelector("57R"));
await page.waitForFunction(
`document.querySelector('${getSelector("56R")}').value !== ""`
);
const text = await page.$eval(getSelector("56R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("2");
})
);
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,119 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { closePages, loadAndWait } from "./test_utils.mjs";
async function dispatchRequestTextContent(page, id) {
return page.evaluate(requestId => {
const event = new CustomEvent("requestTextContent", {
bubbles: true,
cancelable: true,
detail: { requestId },
});
window.dispatchEvent(event);
}, id);
}
async function getReportTextData(page) {
await page.waitForFunction(() => window._reportTextData !== undefined);
return page.evaluate(() => {
const data = window._reportTextData;
delete window._reportTextData;
return data;
});
}
describe("PdfTextExtractor", () => {
describe("Simple multi-page document", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("check that all text is extracted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await dispatchRequestTextContent(page, 1);
const { text, requestId } = await getReportTextData(page);
expect(text).toEqual(
[
"Table Of Content",
"Chapter 1 .......................................................... 2",
"Paragraph 1.1 ...................................................... 3",
"page 1 / 3",
"Chapter 1",
"page 2 / 3",
"Paragraph 1.1",
"Powered by TCPDF (www.tcpdf.org)",
"page 3 / 3",
].join("\n")
);
expect(requestId).toEqual(1);
})
);
});
});
describe("Multi-page document, with disableAutoFetch=true set", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
null,
null,
{
disableAutoFetch: true,
disableStream: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("check that all text is extracted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await dispatchRequestTextContent(page, 2);
const { text, requestId } = await getReportTextData(page);
expect(
text.startsWith(
"Trace-based Just-in-Time Type Specialization for Dynamic\nLanguages"
)
).toBeTrue();
expect(
text.endsWith(
"Conference on Virtual Execution Environments, pages 8393. ACM\nPress, 2007."
)
).toBeTrue();
expect(text.length).toEqual(82804);
expect(requestId).toEqual(2);
})
);
});
});
});

View File

@@ -0,0 +1,39 @@
/* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { closePages, getSelector, loadAndWait } from "./test_utils.mjs";
describe("Text field", () => {
describe("Empty text field", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("file_pdfjs_form.pdf", getSelector("7R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the field is empty although its appearance contains a white space", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const text = await page.$eval(getSelector("7R"), el => el.value);
expect(text).withContext(`In ${browserName}`).toEqual("");
})
);
});
});
});

View File

@@ -0,0 +1,399 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
awaitPromise,
closePages,
getEditorSelector,
getRect,
loadAndWait,
switchToEditor,
waitForPointerUp,
} from "./test_utils.mjs";
const switchToHighlight = switchToEditor.bind(null, "Highlight");
describe("Text layer images", () => {
describe("basic", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"images.pdf",
`.page[data-page-number = "1"] .endOfContent`,
undefined,
{
// When running Firefox with Puppeteer, setting the
// devicePixelRatio Puppeteer option does not properly set
// the `window.devicePixelRatio` value. Set it manually.
earlySetup: `() => { window.devicePixelRatio = 1 }`,
},
{ imagesRightClickMinSize: 16 },
{ width: 800, height: 600, devicePixelRatio: 1 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should render images in the text layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const images = await page.$$eval(
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`,
els => els.map(el => JSON.stringify(el.getBoundingClientRect()))
);
expect(images.length).withContext(`In ${browserName}`).toEqual(5);
})
);
});
it("when right-clicking an image it should get the contents", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const imageCanvas = await page.$(
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
);
expect(await page.evaluate(el => el.width, imageCanvas))
.withContext(`Initial width, in ${browserName}`)
.toBe(0);
expect(await page.evaluate(el => el.height, imageCanvas))
.withContext(`Initial height, in ${browserName}`)
.toBe(0);
await imageCanvas.click({ button: "right" });
expect(await page.evaluate(el => el.width, imageCanvas))
.withContext(`Final width, in ${browserName}`)
.toBeGreaterThan(0);
expect(await page.evaluate(el => el.height, imageCanvas))
.withContext(`Final height, in ${browserName}`)
.toBeGreaterThan(0);
expect(
await page.evaluate(el => {
const ctx = el.getContext("2d");
const imageData = ctx.getImageData(0, 0, el.width, el.height);
const pixels = new Uint32Array(imageData.data.buffer);
const firstPixel = pixels[0];
return pixels.some(pixel => pixel !== firstPixel);
}, imageCanvas)
)
.withContext(`Image is not all the same pixel, in ${browserName}`)
.toBe(true);
})
);
});
});
describe("transforms", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"images.pdf",
`.page[data-page-number = "1"] .endOfContent`,
undefined,
{
// When running Firefox with Puppeteer, setting the
// devicePixelRatio Puppeteer option does not properly set
// the `window.devicePixelRatio` value. Set it manually.
earlySetup: `() => { window.devicePixelRatio = 1 }`,
},
{ imagesRightClickMinSize: 16 },
{ width: 800, height: 600, devicePixelRatio: 1 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("the three copies of the PDF.js logo have different rotations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const getRotation = async nth =>
page.evaluate(n => {
const canvas = document.querySelectorAll(
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
)[n];
const cssTransform = getComputedStyle(canvas).transform;
if (cssTransform && cssTransform !== "none") {
const matrixValues = cssTransform
.slice(7, -1)
.split(", ")
.map(parseFloat);
return (
Math.atan2(matrixValues[1], matrixValues[0]) * (180 / Math.PI)
);
}
return 0;
}, nth);
const rotation1 = await getRotation(1);
const rotation2 = await getRotation(2);
const rotation4 = await getRotation(4);
expect(Math.abs(rotation1 - rotation2))
.withContext(`Rotation between 1 and 2, in ${browserName}`)
.toBeGreaterThan(10);
expect(Math.abs(rotation1 - rotation4))
.withContext(`Rotation between 1 and 4, in ${browserName}`)
.toBeGreaterThan(10);
expect(Math.abs(rotation2 - rotation4))
.withContext(`Rotation between 2 and 4, in ${browserName}`)
.toBeGreaterThan(10);
})
);
});
it("the three copies of the PDF.js logo have the same size", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const getSize = async nth =>
page.evaluate(n => {
const canvas = document.querySelectorAll(
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
)[n];
return { width: canvas.width, height: canvas.height };
}, nth);
const size1 = await getSize(1);
const size2 = await getSize(2);
const size4 = await getSize(4);
const EPSILON = 3;
expect(size1.width)
.withContext(`1-2 width, in ${browserName}`)
.toBeCloseTo(size2.width, EPSILON);
expect(size1.height)
.withContext(`1-2 height, in ${browserName}`)
.toBeCloseTo(size2.height, EPSILON);
expect(size1.width)
.withContext(`1-4 width, in ${browserName}`)
.toBeCloseTo(size4.width, EPSILON);
expect(size1.height)
.withContext(`1-4 height, in ${browserName}`)
.toBeCloseTo(size4.height, EPSILON);
})
);
});
});
describe("trimming", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug_jpx.pdf",
`.page[data-page-number = "1"] .endOfContent`,
undefined,
{
// When running Firefox with Puppeteer, setting the
// devicePixelRatio Puppeteer option does not properly set
// the `window.devicePixelRatio` value. Set it manually.
earlySetup: `() => { window.devicePixelRatio = 1 }`,
},
{ imagesRightClickMinSize: 16 },
{ width: 800, height: 600, devicePixelRatio: 1 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("no white border around black image", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const canvasHandle = await page.$(
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
);
await canvasHandle.click({ button: "right" });
expect(
await page.evaluate(el => {
const ctx = el.getContext("2d");
const imageData = ctx.getImageData(0, 0, el.width, el.height);
const pixels = new Uint32Array(imageData.data.buffer);
return Array.from(pixels.filter(pixel => pixel !== 0xff000000));
}, canvasHandle)
)
.withContext(`Image is all black, in ${browserName}`)
.toEqual([]);
})
);
});
});
describe("trimming after rotation", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"image-rotated-black-white-ratio.pdf",
`.page[data-page-number = "1"] .endOfContent`,
undefined,
{
// When running Firefox with Puppeteer, setting the
// devicePixelRatio Puppeteer option does not properly set
// the `window.devicePixelRatio` value. Set it manually.
earlySetup: `() => { window.devicePixelRatio = 1 }`,
},
{ imagesRightClickMinSize: 16 },
{ width: 800, height: 600, devicePixelRatio: 1 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("no white extra white around rotated image", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const canvasHandle = await page.$(
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
);
await canvasHandle.click({ button: "right" });
expect(
await page.evaluate(el => {
const ctx = el.getContext("2d");
const imageData = ctx.getImageData(0, 0, el.width, el.height);
const pixels = new Uint32Array(imageData.data.buffer);
const blackPixels = pixels.filter(
pixel => pixel === 0xff000000
).length;
const whitePixels = pixels.filter(
pixel => pixel === 0xffffffff
).length;
return blackPixels / (blackPixels + whitePixels);
}, canvasHandle)
)
.withContext(`Image is 75% black, in ${browserName}`)
.toBeCloseTo(0.75);
})
);
});
});
describe("free-highlighting on top of an image placeholder", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"images.pdf",
`.page[data-page-number = "1"] .endOfContent`,
undefined,
{
earlySetup: `() => { window.devicePixelRatio = 1 }`,
},
{
imagesRightClickMinSize: 16,
highlightEditorColors: "yellow=#FFFF00",
},
{ width: 800, height: 600, devicePixelRatio: 1 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must create a free highlight when dragging on an image placeholder (bug 2034980)", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getRect(
page,
`.page[data-page-number="1"] > .textLayer > .textLayerImages > canvas`
);
const x1 = rect.x + rect.width / 4;
const y1 = rect.y + rect.height / 4;
const x2 = rect.x + (3 * rect.width) / 4;
const y2 = rect.y + (3 * rect.height) / 4;
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(x1, y1);
await page.mouse.down();
await page.mouse.move(x2, y2);
await page.mouse.up();
await awaitPromise(clickHandle);
await page.waitForSelector(getEditorSelector(0));
})
);
});
});
describe("free-highlighting on a page with no images", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"empty.pdf",
`.page[data-page-number = "1"] .textLayerImages`,
undefined,
undefined,
{
imagesRightClickMinSize: 16,
highlightEditorColors: "yellow=#FFFF00",
},
{ width: 800, height: 600, devicePixelRatio: 1 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must create a free highlight when dragging on the empty image container", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getRect(
page,
`.page[data-page-number="1"] > .textLayer > .textLayerImages`
);
const x1 = rect.x + rect.width / 4;
const y1 = rect.y + rect.height / 4;
const x2 = rect.x + (3 * rect.width) / 4;
const y2 = rect.y + (3 * rect.height) / 4;
const clickHandle = await waitForPointerUp(page);
await page.mouse.move(x1, y1);
await page.mouse.down();
await page.mouse.move(x2, y2);
await page.mouse.up();
await awaitPromise(clickHandle);
await page.waitForSelector(getEditorSelector(0));
})
);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,547 @@
/* Copyright 2026 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
awaitPromise,
closePages,
FSI,
getThumbnailSelector,
kbFocusNext,
loadAndWait,
PDI,
showViewsManager,
} from "./test_utils.mjs";
function waitForThumbnailVisible(page, pageNum) {
return page.waitForSelector(getThumbnailSelector(pageNum), { visible: true });
}
async function waitForMenu(page, buttonSelector, visible = true) {
return page.waitForFunction(
(selector, vis) => {
const button = document.querySelector(selector);
if (!button) {
return false;
}
return button.getAttribute("aria-expanded") === (vis ? "true" : "false");
},
{},
buttonSelector,
visible
);
}
describe("PDF Thumbnail View", () => {
describe("Works without errors", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", "#viewsManagerToggleButton");
});
afterEach(async () => {
await closePages(pages);
});
it("should render thumbnails without errors", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
const thumbSelector =
"#thumbnailsView .thumbnailImageContainer > img";
await page.waitForSelector(thumbSelector, { visible: true });
await waitForThumbnailVisible(page, 1);
await page.waitForSelector(`${thumbSelector}[src^="blob:http:"]`, {
visible: true,
});
const title = await page.$eval(
getThumbnailSelector(1),
el => el.title
);
expect(title)
.withContext(`In ${browserName}`)
.toBe(`Page ${FSI}1${PDI} of ${FSI}14${PDI}`);
})
);
});
it("should have accessible label on resizer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
const ariaLabel = await page.$eval("#viewsManagerResizer", el =>
el.getAttribute("aria-label")
);
expect(ariaLabel)
.withContext(`In ${browserName}`)
.toBe("Sidebar resizer");
})
);
});
});
describe("The view is scrolled correctly", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", "#viewsManagerToggleButton");
});
afterEach(async () => {
await closePages(pages);
});
async function goToPage(page, number) {
const handle = await page.evaluateHandle(
num => [
new Promise(resolve => {
const container = document.getElementById("viewsManagerContent");
container.addEventListener("scrollend", resolve, { once: true });
// eslint-disable-next-line no-undef
PDFViewerApplication.pdfLinkService.goToPage(num);
}),
],
number
);
return awaitPromise(handle);
}
it("should scroll the view", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
for (const pageNum of [14, 1, 13, 2]) {
await goToPage(page, pageNum);
const thumbSelector = getThumbnailSelector(pageNum);
await page.waitForSelector(
`.thumbnail ${thumbSelector}[aria-current="page"]`,
{ visible: true }
);
await page.waitForSelector(
`${thumbSelector} > img[src^="blob:http:"]`,
{
visible: true,
}
);
}
})
);
});
});
describe("The view is accessible with the keyboard", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", "#viewsManagerToggleButton");
});
afterEach(async () => {
await closePages(pages);
});
it("should navigate with the keyboard", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
await waitForThumbnailVisible(page, 2);
await waitForThumbnailVisible(page, 3);
await kbFocusNext(page, "#viewsManagerSelectorButton");
await kbFocusNext(page, "#viewsManagerStatusActionButton");
await kbFocusNext(page, `#thumbnailsView ${getThumbnailSelector(1)}`);
await page.keyboard.press("ArrowDown");
await page.waitForSelector(
`#thumbnailsView ${getThumbnailSelector(2)}:focus`,
{ visible: true }
);
await page.keyboard.press("ArrowUp");
await page.waitForSelector(`${getThumbnailSelector(1)}:focus`, {
visible: true,
});
await page.keyboard.press("ArrowDown");
await page.keyboard.press("ArrowDown");
await page.waitForSelector(
`#thumbnailsView ${getThumbnailSelector(3)}:focus`,
{ visible: true }
);
await page.keyboard.press("Enter");
const currentPage = await page.$eval(
"#pageNumber",
el => el.valueAsNumber
);
expect(currentPage).withContext(`In ${browserName}`).toBe(3);
await page.keyboard.press("End");
await page.waitForSelector(
`#thumbnailsView ${getThumbnailSelector(14)}:focus`,
{ visible: true }
);
await page.keyboard.press("Home");
await page.waitForSelector(
`#thumbnailsView ${getThumbnailSelector(1)}:focus`,
{ visible: true }
);
})
);
});
it("must navigate when a synthetic click is dispatched on the thumbnail image (bug 2034568)", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 3);
// Simulate a screen reader (e.g. NVDA) firing a synthetic click on
// the <img> child rather than the thumbnailImageContainer button.
await page.evaluate(() => {
const img = document.querySelector(
`.thumbnail[page-number="3"] .thumbnailImageContainer img`
);
img.dispatchEvent(new MouseEvent("click", { bubbles: true }));
});
const currentPage = await page.$eval(
"#pageNumber",
el => el.valueAsNumber
);
expect(currentPage).withContext(`In ${browserName}`).toBe(3);
})
);
});
});
describe("The manage dropdown menu", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
"#viewsManagerToggleButton",
null,
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
async function enableMenuItems(page) {
await page.evaluate(() => {
document
.querySelectorAll("#viewsManagerStatusActionOptions button")
.forEach(button => {
button.disabled = false;
});
});
}
it("should open with Enter key and remain open", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
await enableMenuItems(page);
// Focus the manage button
await kbFocusNext(page, "#viewsManagerStatusActionButton");
// Press Enter to open the menu
await page.keyboard.press("Enter");
await waitForMenu(page, "#viewsManagerStatusActionButton");
// Verify first menu item can be focused
await page.waitForSelector("#viewsManagerStatusActionCopy:focus", {
visible: true,
});
// Close menu with Escape
await page.keyboard.press("Escape");
await waitForMenu(page, "#viewsManagerStatusActionButton", false);
})
);
});
it("should open with Space key and remain open", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
await enableMenuItems(page);
// Focus the manage button
await kbFocusNext(page, "#viewsManagerStatusActionButton");
// Press Space to open the menu
await page.keyboard.press(" ");
await waitForMenu(page, "#viewsManagerStatusActionButton");
// Verify first menu item can be focused
await page.waitForSelector("#viewsManagerStatusActionCopy:focus", {
visible: true,
});
// Navigate menu items with arrow keys
await page.keyboard.press("ArrowDown");
await page.waitForSelector("#viewsManagerStatusActionCut:focus", {
visible: true,
});
// Menu should still be open
await waitForMenu(page, "#viewsManagerStatusActionButton");
// Close menu with Escape
await page.keyboard.press("Escape");
await waitForMenu(page, "#viewsManagerStatusActionButton", false);
})
);
});
});
describe("Checkbox accessibility", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
"#viewsManagerToggleButton",
null,
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should have a title on the checkbox", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
const title = await page.$eval(
`.thumbnail[page-number="1"] input[type="checkbox"]`,
el => el.title
);
expect(title)
.withContext(`In ${browserName}`)
.toBe(`Select page ${FSI}1${PDI}`);
})
);
});
});
describe("Menu keyboard navigation with multi-character keys (bug 2016212)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"page_with_number_and_link.pdf",
"#viewsManagerSelectorButton",
null,
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must navigate menus with ArrowDown and Tab keys", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
// Focus the views manager selector button
await page.waitForSelector("#viewsManagerSelectorButton", {
visible: true,
});
await page.focus("#viewsManagerSelectorButton");
// Open menu with Enter key
await page.keyboard.press("Enter");
// Wait for menu to be expanded
await waitForMenu(page, "#viewsManagerSelectorButton");
// Check that focus moved to the first menu button (pages)
await page.waitForSelector("#thumbnailsViewMenu:focus", {
visible: true,
});
// Press ArrowDown to navigate to second item
await page.keyboard.press("ArrowDown");
// Should now be on outlines button
await page.waitForSelector("#outlinesViewMenu:focus", {
visible: true,
});
// Press Tab to move to the manage button (should close views menu)
await kbFocusNext(page, "#viewsManagerStatusActionButton");
// Wait for views manager menu to be collapsed
await waitForMenu(page, "#viewsManagerSelectorButton", false);
// Open manage menu with Space key
await page.keyboard.press(" ");
// Wait for manage menu to be expanded
await waitForMenu(page, "#viewsManagerStatusActionButton");
})
);
});
});
describe("Views manager status visibility (bug 2016656)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"page_with_number_and_link.pdf",
"#viewsManagerToggleButton",
null,
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should show the manage button in thumbnail view and hide it in outline view", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
// The status bar (Select pages + Manage button) must be visible in
// thumbnail view.
await page.waitForSelector("#viewsManagerStatus", { visible: true });
// Switch to outline view.
await page.click("#viewsManagerSelectorButton");
await page.waitForSelector("#outlinesViewMenu", { visible: true });
await page.click("#outlinesViewMenu");
await page.waitForSelector("#outlinesView", { visible: true });
// The status bar must no longer be visible in outline view.
await page.waitForSelector("#viewsManagerStatus", { hidden: true });
})
);
});
});
describe("Checkbox keyboard navigation", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
"#viewsManagerToggleButton",
null,
null,
{ enableSplitMerge: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("should focus checkboxes with Tab key", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
// Focus the first thumbnail button
await kbFocusNext(page, getThumbnailSelector(1));
// Verify we're on the first thumbnail
await page.waitForSelector(`${getThumbnailSelector(1)}:focus`, {
visible: true,
});
// Tab to checkbox
await kbFocusNext(
page,
`.thumbnail[page-number="1"] input[type="checkbox"]`
);
})
);
});
it("should navigate checkboxes with arrow keys", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await showViewsManager(page);
await waitForThumbnailVisible(page, 1);
await waitForThumbnailVisible(page, 2);
// Navigate to first checkbox
await kbFocusNext(
page,
`.thumbnail[page-number="1"] input[type="checkbox"]`
);
// Verify first checkbox is focused
await page.waitForSelector(
`.thumbnail[page-number="1"] input[type="checkbox"]:focus`,
{ visible: true }
);
// Navigate to next checkbox with ArrowDown
await page.keyboard.press("ArrowDown");
await page.waitForSelector(
`.thumbnail[page-number="2"] input[type="checkbox"]:focus`,
{ visible: true }
);
// Navigate back with ArrowUp
await page.keyboard.press("ArrowUp");
await page.waitForSelector(
`.thumbnail[page-number="1"] input[type="checkbox"]:focus`,
{ visible: true }
);
})
);
});
});
});

File diff suppressed because it is too large Load Diff