fork from https://github.com/mozilla/pdf.js.git
This commit is contained in:
610
test/integration/accessibility_spec.mjs
Normal file
610
test/integration/accessibility_spec.mjs
Normal 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"]);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
969
test/integration/annotation_spec.mjs
Normal file
969
test/integration/annotation_spec.mjs
Normal 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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
262
test/integration/autolinker_spec.mjs
Normal file
262
test/integration/autolinker_spec.mjs
Normal 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"));
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
94
test/integration/caret_browsing_spec.mjs
Normal file
94
test/integration/caret_browsing_spec.mjs
Normal 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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
1270
test/integration/comment_spec.mjs
Normal file
1270
test/integration/comment_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
181
test/integration/copy_paste_spec.mjs
Normal file
181
test/integration/copy_paste_spec.mjs
Normal 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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
134
test/integration/cursor_tools_spec.mjs
Normal file
134
test/integration/cursor_tools_spec.mjs
Normal 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);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
429
test/integration/document_properties_spec.mjs
Normal file
429
test/integration/document_properties_spec.mjs
Normal 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);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
234
test/integration/find_spec.mjs
Normal file
234
test/integration/find_spec.mjs
Normal 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();
|
||||
}
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
3682
test/integration/freetext_editor_spec.mjs
Normal file
3682
test/integration/freetext_editor_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
2851
test/integration/highlight_editor_spec.mjs
Normal file
2851
test/integration/highlight_editor_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
1350
test/integration/ink_editor_spec.mjs
Normal file
1350
test/integration/ink_editor_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
115
test/integration/jasmine-boot.js
Normal file
115
test/integration/jasmine-boot.js
Normal 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 };
|
||||
196
test/integration/presentation_mode_spec.mjs
Normal file
196
test/integration/presentation_mode_spec.mjs
Normal 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);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
3725
test/integration/reorganize_pages_spec.mjs
Normal file
3725
test/integration/reorganize_pages_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
2699
test/integration/scripting_spec.mjs
Normal file
2699
test/integration/scripting_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
774
test/integration/signature_editor_spec.mjs
Normal file
774
test/integration/signature_editor_spec.mjs
Normal 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 });
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
107
test/integration/simple_viewer_spec.mjs
Normal file
107
test/integration/simple_viewer_spec.mjs
Normal 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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
1891
test/integration/stamp_editor_spec.mjs
Normal file
1891
test/integration/stamp_editor_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
1209
test/integration/test_utils.mjs
Normal file
1209
test/integration/test_utils.mjs
Normal file
File diff suppressed because it is too large
Load Diff
119
test/integration/text_extractor_spec.mjs
Normal file
119
test/integration/text_extractor_spec.mjs
Normal 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 83–93. ACM\nPress, 2007."
|
||||
)
|
||||
).toBeTrue();
|
||||
expect(text.length).toEqual(82804);
|
||||
expect(requestId).toEqual(2);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
39
test/integration/text_field_spec.mjs
Normal file
39
test/integration/text_field_spec.mjs
Normal 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("");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
399
test/integration/text_layer_images_spec.mjs
Normal file
399
test/integration/text_layer_images_spec.mjs
Normal 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));
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
1357
test/integration/text_layer_spec.mjs
Normal file
1357
test/integration/text_layer_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
547
test/integration/thumbnail_view_spec.mjs
Normal file
547
test/integration/thumbnail_view_spec.mjs
Normal 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 }
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
2069
test/integration/viewer_spec.mjs
Normal file
2069
test/integration/viewer_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user