first commit
Some checks failed
Types tests / Test (lts/*) (push) Has been cancelled
Lint / Lint (lts/*) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CI / Test (20) (push) Has been cancelled
CI / Test (22) (push) Has been cancelled
CI / Test (24) (push) Has been cancelled

This commit is contained in:
2025-10-03 22:20:19 +08:00
commit 44db9807a1
2172 changed files with 526822 additions and 0 deletions

View File

@@ -0,0 +1,305 @@
/* 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");
});
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("");
})
);
});
});
});

View File

@@ -0,0 +1,881 @@
/* 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`);
}
})
);
});
});
});
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;
})
);
});
});
});
});

View File

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

View File

@@ -0,0 +1,97 @@
/* 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 } from "./test_utils.mjs";
const waitForSelectionChange = (page, selection) =>
page.waitForFunction(
// We need to replace EOL on Windows to make the test pass.
sel => document.getSelection().toString().replaceAll("\r\n", "\n") === sel,
{},
selection
);
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");
})
);
});
});
});

View File

@@ -0,0 +1,627 @@
/* 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,
dragAndDrop,
getEditorSelector,
getRect,
getSpanRectFromText,
loadAndWait,
scrollIntoView,
selectEditor,
switchToEditor,
waitAndClick,
waitForSerialized,
} from "./test_utils.mjs";
const switchToHighlight = switchToEditor.bind(null, "Highlight");
const switchToStamp = switchToEditor.bind(null, "Stamp");
const switchToComment = switchToEditor.bind(null, "Comment");
const highlightSpan = async (page, pageIndex, text) => {
const rect = await getSpanRectFromText(page, pageIndex, text);
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
};
const editComment = async (page, editorSelector, comment) => {
const commentButtonSelector = `${editorSelector} button.comment`;
await waitAndClick(page, commentButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, comment);
await waitAndClick(page, "#commentManagerSaveButton");
await page.waitForSelector("#commentManagerDialog", {
visible: false,
});
};
describe("Comment", () => {
describe("Comment edit dialog must be visible in ltr", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1989304.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must set the comment dialog in the viewport (LTR)", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
await scrollIntoView(page, ".textLayer span:last-of-type");
const rect = await getSpanRectFromText(page, 1, "...");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
// Here and elsewhere, we add a small delay between press and release
// to make sure that a pointerup event is triggered after
// selectionchange.
// It works with a value of 1ms, but we use 100ms to be sure.
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
await waitAndClick(page, commentButtonSelector);
await page.waitForSelector("#commentManagerDialog", {
visible: true,
});
const dialogRect = await getRect(page, "#commentManagerDialog");
const viewport = await page.evaluate(() => ({
width: document.documentElement.clientWidth,
height: document.documentElement.clientHeight,
}));
expect(dialogRect.x + dialogRect.width)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(viewport.width + 1);
expect(dialogRect.y + dialogRect.height)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(viewport.height + 1);
})
);
});
});
describe("Comment edit dialog must be visible in rtl", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"bug1989304.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true, localeProperties: "ar" }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must set the comment dialog in the viewport (RTL)", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
await scrollIntoView(page, ".textLayer span:nth-of-type(4)");
const rect = await getSpanRectFromText(page, 1, "World");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
await waitAndClick(page, commentButtonSelector);
await page.waitForSelector("#commentManagerDialog", {
visible: true,
});
const dialogRect = await getRect(page, "#commentManagerDialog");
const viewport = await page.evaluate(() => ({
height: window.innerHeight,
}));
expect(dialogRect.x + dialogRect.width)
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(-1);
expect(dialogRect.y + dialogRect.height)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(viewport.height + 1);
})
);
});
});
describe("Update comment position and color in reading mode", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"comments.pdf",
".annotationEditorLayer",
"page-fit",
null,
{
enableComment: true,
highlightEditorColors:
"yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0000",
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must set the comment button at the right place", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
const stampSelector = getEditorSelector(8);
await selectEditor(page, stampSelector);
await dragAndDrop(page, stampSelector, [[100, 100]]);
await waitForSerialized(page, 1);
const rectCommentButton = await getRect(
page,
`${stampSelector} .annotationCommentButton`
);
await switchToStamp(page, /* disable = */ true);
const rectCommentButtonAfter = await getRect(
page,
`#pdfjs_internal_id_713R + .annotationCommentButton`
);
expect(Math.abs(rectCommentButtonAfter.x - rectCommentButton.x))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(Math.abs(rectCommentButtonAfter.y - rectCommentButton.y))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(
Math.abs(rectCommentButtonAfter.width - rectCommentButton.width)
)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(
Math.abs(rectCommentButtonAfter.height - rectCommentButton.height)
)
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
})
);
});
it("must set the right color to the comment button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const highlightSelector = getEditorSelector(0);
await selectEditor(page, highlightSelector);
const colorButtonSelector = `${highlightSelector} .editToolbar button`;
await page.waitForSelector(`${colorButtonSelector}.colorPicker`);
await page.click(`${colorButtonSelector}.colorPicker`);
await page.waitForSelector(`${colorButtonSelector}[title = "Red"]`);
await page.click(`${colorButtonSelector}[title = "Red"]`);
await page.waitForSelector(
`.page[data-page-number = "1"] svg.highlight[fill = "#FF0000"]`
);
const commentButtonColor = await page.evaluate(selector => {
const button = document.querySelector(
`${selector} .annotationCommentButton`
);
return window.getComputedStyle(button).backgroundColor;
}, highlightSelector);
await switchToHighlight(page, /* disable = */ true);
const commentButtonColorAfter = await page.evaluate(() => {
const button = document.querySelector(
"section[data-annotation-id='612R'] + .annotationCommentButton"
);
return window.getComputedStyle(button).backgroundColor;
});
expect(commentButtonColorAfter)
.withContext(`In ${browserName}`)
.toEqual(commentButtonColor);
})
);
});
});
describe("Comment buttons", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the comment button has a title", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Languages");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
let commentButtonSelector = `${getEditorSelector(0)} button.comment`;
await page.waitForSelector(commentButtonSelector, { visible: true });
let title = await page.evaluate(
selector => document.querySelector(selector).title,
commentButtonSelector
);
expect(title)
.withContext(`In ${browserName}`)
.toEqual("Edit comment");
await page.click(commentButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, "Hello world!");
await page.click("#commentManagerSaveButton");
commentButtonSelector = `${getEditorSelector(0)} button.annotationCommentButton`;
await page.waitForSelector(commentButtonSelector, {
visible: true,
});
title = await page.evaluate(selector => {
const button = document.querySelector(selector);
return button.title;
}, commentButtonSelector);
expect(title)
.withContext(`In ${browserName}`)
.toEqual("Show comment");
})
);
});
it("must check that the comment button is added in the annotation layer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Abstract");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
const comment = "Hello world!";
await editComment(page, getEditorSelector(0), comment);
await page.hover("#editorHighlightButton");
let buttonSelector =
".annotationEditorLayer .annotationCommentButton";
await page.waitForSelector(buttonSelector, { visible: true });
await page.hover(buttonSelector);
const popupSelector = "#commentPopup";
await page.waitForSelector(popupSelector, {
visible: true,
});
let popupText = await page.evaluate(
selector => document.querySelector(selector).textContent,
`${popupSelector} .commentPopupText`
);
expect(popupText).withContext(`In ${browserName}`).toEqual(comment);
await page.hover("#editorHighlightButton");
await switchToHighlight(page, /* disable = */ true);
buttonSelector = ".annotationLayer .annotationCommentButton";
await page.waitForSelector(buttonSelector, {
visible: true,
});
await page.hover(buttonSelector);
await page.waitForSelector(popupSelector, {
visible: true,
});
popupText = await page.evaluate(
selector => document.querySelector(selector).textContent,
`${popupSelector} .commentPopupText`
);
expect(popupText).withContext(`In ${browserName}`).toEqual(comment);
})
);
});
});
describe("Focused element after editing", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the focus is moved on the comment button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToHighlight(page);
const rect = await getSpanRectFromText(page, 1, "Languages");
const x = rect.x + rect.width / 2;
const y = rect.y + rect.height / 2;
await page.mouse.click(x, y, { count: 2, delay: 100 });
await page.waitForSelector(getEditorSelector(0));
const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
await waitAndClick(page, commentButtonSelector);
await page.waitForSelector("#commentManagerCancelButton", {
visible: true,
});
const handle = await createPromise(page, resolve => {
document
.querySelector("button.comment")
.addEventListener("focus", resolve, { once: true });
});
await page.click("#commentManagerCancelButton");
await awaitPromise(handle);
await waitAndClick(page, commentButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, "Hello world!");
await page.click("#commentManagerSaveButton");
await page.waitForSelector("button.annotationCommentButton", {
visible: true,
});
await page.waitForFunction(
sel => document.activeElement === document.querySelector(sel),
{},
"button.annotationCommentButton"
);
})
);
});
});
describe("Focused element after editing in reading mode", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"comments.pdf",
".annotationLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the focus is moved on the comment button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const commentButtonSelector = `[data-annotation-id="612R"] + button.annotationCommentButton`;
await waitAndClick(page, commentButtonSelector);
const commentPopupSelector = "#commentPopup";
const editButtonSelector = `${commentPopupSelector} button.commentPopupEdit`;
await waitAndClick(page, editButtonSelector);
await page.waitForSelector("#commentManagerCancelButton", {
visible: true,
});
let handle = await createPromise(page, resolve => {
document
.querySelector(
`[data-annotation-id="612R"] + button.annotationCommentButton`
)
.addEventListener("focus", resolve, { once: true });
});
await page.click("#commentManagerCancelButton");
await awaitPromise(handle);
await waitAndClick(page, commentButtonSelector);
await waitAndClick(page, editButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, "Hello world!");
handle = await createPromise(page, resolve => {
document
.querySelector(
`[data-annotation-id="612R"] + button.annotationCommentButton`
)
.addEventListener("focus", resolve, { once: true });
});
await page.click("#commentManagerSaveButton");
await awaitPromise(handle);
})
);
});
});
describe("Comment sidebar", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"comments.pdf",
".annotationEditorLayer",
"page-width",
null,
{ enableComment: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the comment sidebar is resizable", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToComment(page);
const sidebarSelector = "#editorCommentParamsToolbar";
for (const extraWidth of [100, -100]) {
const rect = await getRect(page, sidebarSelector);
const resizerRect = await getRect(
page,
"#editorCommentsSidebarResizer"
);
const startX = resizerRect.x + resizerRect.width / 2;
const startY = resizerRect.y + 2;
await page.mouse.move(startX, startY);
await page.mouse.down();
const steps = 20;
await page.mouse.move(startX - extraWidth, startY, { steps });
await page.mouse.up();
const rectAfter = await getRect(page, sidebarSelector);
expect(Math.abs(rectAfter.width - (rect.width + extraWidth)))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(Math.abs(rectAfter.x - (rect.x - extraWidth)))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
}
})
);
});
it("must check that comments are in chronological order", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToComment(page);
const checkDates = async () => {
const dates = await page.evaluate(() =>
Array.from(
document.querySelectorAll(
`#editorCommentParamsToolbar ul > li > time`
)
).map(time => new Date(time.getAttribute("datetime")))
);
for (let i = 0; i < dates.length - 1; i++) {
expect(dates[i])
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(dates[i + 1]);
}
};
await checkDates();
// Add an highlight with a comment and check the order again.
await switchToHighlight(page);
await highlightSpan(page, 1, "Languages");
const editorSelector = getEditorSelector(9);
await page.waitForSelector(editorSelector);
const commentButtonSelector = `${editorSelector} button.comment`;
await waitAndClick(page, commentButtonSelector);
const textInputSelector = "#commentManagerTextInput";
await page.waitForSelector(textInputSelector, {
visible: true,
});
await page.type(textInputSelector, "Hello world!");
await page.click("#commentManagerSaveButton");
await waitForSerialized(page, 1);
await switchToComment(page);
await checkDates();
})
);
});
it("must check that comments can be selected/unselected", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToComment(page);
const firstElementSelector =
"#editorCommentsSidebarList li:first-child";
await waitAndClick(page, firstElementSelector);
const popupSelector = "#commentPopup";
await page.waitForSelector(popupSelector, { visible: true });
const popupTextSelector = `${popupSelector} .commentPopupText`;
await page.waitForSelector(popupTextSelector, {
visible: true,
});
const popupText = await page.evaluate(
selector => document.querySelector(selector).textContent,
popupTextSelector
);
expect(popupText)
.withContext(`In ${browserName}`)
.toEqual("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
// Click again to unselect the comment.
await waitAndClick(page, firstElementSelector);
await page.waitForSelector(popupSelector, { visible: false });
})
);
});
});
});

View File

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

View File

@@ -0,0 +1,97 @@
/* 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";
const FIELDS = [
"fileName",
"fileSize",
"title",
"author",
"subject",
"keywords",
"creationDate",
"modificationDate",
"creator",
"producer",
"version",
"pageCount",
"pageSize",
"linearized",
];
describe("PDFDocumentProperties", () => {
async function getFieldProperties(page) {
const promises = [];
for (const name of FIELDS) {
promises.push(
page.evaluate(
n => [n, document.getElementById(`${n}Field`).textContent],
name
)
);
}
return Object.fromEntries(await Promise.all(promises));
}
describe("Document with both /Info and /Metadata", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the document properties dialog has the correct information", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { hidden: false });
await page.click("#documentProperties");
await page.waitForSelector("#documentPropertiesDialog", {
hidden: false,
});
await page.waitForFunction(
`document.getElementById("fileSizeField").textContent !== "-"`
);
const props = await getFieldProperties(page);
expect(props).toEqual({
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",
});
})
);
});
});
});

View File

@@ -0,0 +1,180 @@
/* 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 } 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 page.waitForFunction(
() => !!document.querySelector("#findResultsCount")?.textContent
);
const resultElement = await page.waitForSelector("#findResultsCount");
const resultText = await resultElement.evaluate(el => el.textContent);
expect(resultText).toEqual(`${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();
})
);
});
});
});

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,83 @@
/* 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 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",
"document_properties_spec.mjs",
"find_spec.mjs",
"freetext_editor_spec.mjs",
"highlight_editor_spec.mjs",
"ink_editor_spec.mjs",
"scripting_spec.mjs",
"signature_editor_spec.mjs",
"stamp_editor_spec.mjs",
"text_field_spec.mjs",
"text_layer_spec.mjs",
"thumbnail_view_spec.mjs",
"viewer_spec.mjs",
],
});
jasmine.addReporter({
jasmineDone(suiteInfo) {},
jasmineStarted(suiteInfo) {},
specDone(result) {
// Report on the result of individual tests.
++results.runs;
if (result.failedExpectations.length > 0) {
++results.failures;
console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
} else {
console.log(`TEST-PASSED | ${result.description}`);
}
},
specStarted(result) {},
suiteDone(result) {
// Report on the result of `afterAll` invocations.
if (result.failedExpectations.length > 0) {
++results.failures;
console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
}
},
suiteStarted(result) {
// Report on the result of `beforeAll` invocations.
if (result.failedExpectations.length > 0) {
++results.failures;
console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
}
},
});
return jasmine.execute();
}
export { runTests };

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,774 @@
/* Copyright 2025 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
awaitPromise,
closePages,
copy,
FSI,
getEditorSelector,
getRect,
loadAndWait,
paste,
PDI,
switchToEditor,
waitForPointerUp,
waitForTimeout,
} from "./test_utils.mjs";
import fs from "fs";
import path from "path";
import { PNG } from "pngjs";
const __dirname = import.meta.dirname;
const switchToSignature = switchToEditor.bind(null, "Signature");
describe("Signature Editor", () => {
const descriptionInputSelector = "#addSignatureDescription > input";
const addButtonSelector = "#addSignatureAddButton";
describe("Basic operations", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the editor has been removed when the dialog is cancelled", async () => {
await Promise.all(
pages.map(async ([_, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
// An invisible editor is created but invisible.
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: false });
await page.click("#addSignatureCancelButton");
// The editor should have been removed.
await page.waitForSelector(`:not(${editorSelector})`);
})
);
});
it("must check that the basic and common elements are working as expected", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToSignature(page);
await page.click("#editorSignatureAddSignature");
await page.waitForSelector("#addSignatureDialog", {
visible: true,
});
await page.waitForSelector(
"#addSignatureTypeButton[aria-selected=true]"
);
await page.click("#addSignatureTypeInput");
await page.waitForSelector(
"#addSignatureSaveContainer > input:disabled"
);
let description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("");
await page.waitForSelector(`${addButtonSelector}:disabled`);
await page.waitForSelector("#addSignatureDescInput:disabled");
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
await page.waitForSelector("#addSignatureDescInput:not(:disabled)");
// The save button should be enabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:not(:disabled)"
);
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
// The description has been filled in automatically.
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("PDF.js");
// Clear the description.
await page.click("#addSignatureDescription > button");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
// Clear the text for the signature.
await page.click("#clearSignatureButton");
await page.waitForFunction(
`document.querySelector("#addSignatureTypeInput").value === ""`
);
// The save button should be disabled now.
await page.waitForSelector(
"#addSignatureSaveContainer > input:disabled"
);
await page.waitForSelector(`${addButtonSelector}:disabled`);
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
// Clearing the signature type should clear the description.
await page.click("#clearSignatureButton");
await page.waitForFunction(
`document.querySelector("#addSignatureTypeInput").value === ""`
);
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
// Add a signature and change the description.
await page.type("#addSignatureTypeInput", "PDF.js");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value !== ""`
);
await page.click("#addSignatureDescription > button");
await page.waitForFunction(
`document.querySelector("${descriptionInputSelector}").value === ""`
);
await page.type(descriptionInputSelector, "Hello World");
await page.type("#addSignatureTypeInput", "Hello");
// The description mustn't be changed.
// eslint-disable-next-line no-restricted-syntax
await waitForTimeout(100);
description = await page.$eval(
descriptionInputSelector,
el => el.value
);
expect(description).withContext(browserName).toEqual("Hello World");
await page.click("#addSignatureAddButton");
await page.waitForSelector("#addSignatureDialog", {
visible: false,
});
const editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(
`.canvasWrapper > svg use[href="#path_p1_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_p1_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_p1_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_p1_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_p1_0"]`,
{ visible: true }
);
const color = await page.evaluate(() => {
const use = document.querySelector(
`.canvasWrapper > svg use[href="#path_p1_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_p1_0"]`,
{ visible: true }
);
const color = await page.evaluate(() => {
const use = document.querySelector(
`.canvasWrapper > svg use[href="#path_p1_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_p1_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 });
})
);
});
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,964 @@
/* 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 os from "os";
const isMac = os.platform() === "darwin";
function loadAndWait(filename, selector, zoom, setups, options, viewport) {
return Promise.all(
global.integrationSessions.map(async session => {
const page = await session.browser.newPage();
if (viewport) {
await page.setViewport(viewport);
}
// In order to avoid errors because of checks which depend on
// a locale.
await page.evaluateOnNewDocument(() => {
Object.defineProperty(navigator, "language", {
get() {
return "en-US";
},
});
Object.defineProperty(navigator, "languages", {
get() {
return ["en-US", "en"];
},
});
});
let app_options = "";
if (options) {
const optionsObject =
typeof options === "function"
? await options(page, session.name)
: options;
// Options must be handled in app.js::_parseHashParams.
for (const [key, value] of Object.entries(optionsObject)) {
app_options += `&${key}=${encodeURIComponent(value)}`;
}
}
const fileParam = filename.startsWith("http")
? filename
: `/test/pdfs/${filename}`;
const url = `${global.integrationBaseUrl}?file=${fileParam}#zoom=${zoom ?? "page-fit"}${app_options}`;
if (setups) {
// page.evaluateOnNewDocument allows us to run code before the
// first js script is executed.
// The idea here is to set up some setters for PDFViewerApplication
// and EventBus, so we can inject some code to do whatever we want
// soon enough especially before the first event in the eventBus is
// dispatched.
const { prePageSetup, appSetup, earlySetup, eventBusSetup } = setups;
await prePageSetup?.(page);
if (earlySetup || appSetup || eventBusSetup) {
await page.evaluateOnNewDocument(
(eaSetup, aSetup, evSetup) => {
if (eaSetup) {
// eslint-disable-next-line no-eval
eval(`(${eaSetup})`)();
}
let app;
let eventBus;
Object.defineProperty(window, "PDFViewerApplication", {
get() {
return app;
},
set(newValue) {
app = newValue;
if (aSetup) {
// eslint-disable-next-line no-eval
eval(`(${aSetup})`)(app);
}
Object.defineProperty(app, "eventBus", {
get() {
return eventBus;
},
set(newV) {
eventBus = newV;
if (evSetup) {
// eslint-disable-next-line no-eval
eval(`(${evSetup})`)(eventBus);
}
},
});
},
});
},
earlySetup?.toString(),
appSetup?.toString(),
eventBusSetup?.toString()
);
}
}
await page.goto(url);
await setups?.postPageSetup?.(page);
await page.bringToFront();
if (selector) {
await page.waitForSelector(selector, {
timeout: 0,
});
}
return [session.name, page];
})
);
}
function createPromise(page, callback) {
return page.evaluateHandle(
// eslint-disable-next-line no-eval
cb => [new Promise(eval(`(${cb})`))],
callback.toString()
);
}
function awaitPromise(promise) {
return promise.evaluate(([p]) => p);
}
function closePages(pages) {
return Promise.all(pages.map(([_, page]) => closeSinglePage(page)));
}
async function closeSinglePage(page) {
// Avoid to keep something from a previous test.
await page.evaluate(async () => {
await window.PDFViewerApplication.testingClose();
window.localStorage.clear();
});
await page.close({ runBeforeUnload: false });
}
async function waitForSandboxTrip(page) {
const handle = await page.evaluateHandle(() => [
new Promise(resolve => {
window.addEventListener("sandboxtripend", resolve, { once: true });
window.PDFViewerApplication.pdfScriptingManager.sandboxTrip();
}),
]);
await awaitPromise(handle);
}
function waitForTimeout(milliseconds) {
/**
* Wait for the given number of milliseconds.
*
* Note that waiting for an arbitrary time in tests is discouraged because it
* can easily cause intermittent failures, which is why this functionality is
* no longer provided by Puppeteer 22+ and we have to implement it ourselves
* for the remaining callers in the integration tests. We should avoid
* creating new usages of this function; instead please refer to the better
* alternatives at https://github.com/puppeteer/puppeteer/pull/11780.
*/
return new Promise(resolve => {
setTimeout(resolve, milliseconds);
});
}
async function clearInput(page, selector, waitForInputEvent = false) {
const action = async () => {
await page.click(selector);
await kbSelectAll(page);
await page.keyboard.press("Backspace");
await page.waitForFunction(
`document.querySelector('${selector}').value === ""`
);
};
return waitForInputEvent
? waitForEvent({
page,
eventName: "input",
action,
selector,
})
: action();
}
async function waitAndClick(page, selector, clickOptions = {}) {
await page.waitForSelector(selector, { visible: true });
await page.click(selector, clickOptions);
}
function waitForPointerUp(page) {
return createPromise(page, resolve => {
window.addEventListener("pointerup", resolve, { once: true });
});
}
function getSelector(id) {
return `[data-element-id="${id}"]`;
}
async function getRect(page, selector) {
// In Chrome something is wrong when serializing a `DomRect`,
// so we extract the values and return them ourselves.
await page.waitForSelector(selector, { visible: true });
return page.$eval(selector, el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
}
function getQuerySelector(id) {
return `document.querySelector('${getSelector(id)}')`;
}
function getComputedStyleSelector(id) {
return `getComputedStyle(${getQuerySelector(id)})`;
}
function getEditorSelector(n) {
return `#pdfjs_internal_editor_${n}`;
}
function getAnnotationSelector(id) {
return `[data-annotation-id="${id}"]`;
}
async function getSpanRectFromText(page, pageNumber, text) {
await page.waitForSelector(
`.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
);
return page.evaluate(
(number, content) => {
for (const el of document.querySelectorAll(
`.page[data-page-number="${number}"] > .textLayer span:not(:has(> span))`
)) {
if (el.textContent === content) {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
}
}
return null;
},
pageNumber,
text
);
}
async function waitForEvent({
page,
eventName,
action,
selector = null,
validator = null,
timeout = 5000,
}) {
const handle = await page.evaluateHandle(
(name, sel, validate, timeOut) => {
let callback = null,
timeoutId = null;
const element = sel ? document.querySelector(sel) : document;
return [
Promise.race([
new Promise(resolve => {
// The promise is resolved if the event fired in the context of the
// selector and, if a validator is defined, the event data satisfies
// the conditions of the validator function.
callback = e => {
if (timeoutId) {
clearTimeout(timeoutId);
}
// eslint-disable-next-line no-eval
resolve(validate ? eval(`(${validate})`)(e) : true);
};
element.addEventListener(name, callback, { once: true });
}),
new Promise(resolve => {
timeoutId = setTimeout(() => {
element.removeEventListener(name, callback);
resolve(null);
}, timeOut);
}),
]),
];
},
eventName,
selector,
validator ? validator.toString() : null,
timeout
);
await action();
const success = await awaitPromise(handle);
if (success === null) {
console.warn(
`waitForEvent: ${eventName} didn't trigger within the timeout`
);
} else if (!success) {
console.warn(`waitForEvent: ${eventName} triggered, but validation failed`);
}
}
async function waitForStorageEntries(page, nEntries) {
return page.waitForFunction(
n => window.PDFViewerApplication.pdfDocument.annotationStorage.size === n,
{},
nEntries
);
}
async function waitForSerialized(page, nEntries) {
return page.waitForFunction(
n => {
try {
return (
(window.PDFViewerApplication.pdfDocument.annotationStorage
.serializable.map?.size ?? 0) === n
);
} catch {
// When serializing a stamp annotation with a SVG, the transfer
// can fail because of the SVG, so we just retry.
return false;
}
},
{},
nEntries
);
}
async function applyFunctionToEditor(page, editorId, func) {
return page.evaluate(
(id, f) => {
const editor =
window.PDFViewerApplication.pdfDocument.annotationStorage.getRawValue(
id
);
// eslint-disable-next-line no-eval
eval(`(${f})`)(editor);
},
editorId,
func.toString()
);
}
async function selectEditor(page, selector, count = 1) {
const editorRect = await getRect(page, selector);
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
{ count }
);
await waitForSelectedEditor(page, selector);
}
async function waitForSelectedEditor(page, selector) {
return page.waitForSelector(`${selector}.selectedEditor`);
}
async function unselectEditor(page, selector) {
await page.keyboard.press("Escape");
await waitForUnselectedEditor(page, selector);
}
async function waitForUnselectedEditor(page, selector) {
return page.waitForSelector(`${selector}:not(.selectedEditor)`);
}
async function mockClipboard(pages) {
return Promise.all(
pages.map(async ([_, page]) => {
await page.evaluate(() => {
let data = null;
const clipboard = {
writeText: async text => (data = text),
readText: async () => data,
};
Object.defineProperty(navigator, "clipboard", { value: clipboard });
});
})
);
}
async function copy(page) {
await waitForEvent({
page,
eventName: "copy",
action: () => kbCopy(page),
});
}
async function copyToClipboard(page, data) {
await page.evaluate(async dat => {
const items = Object.create(null);
for (const [type, value] of Object.entries(dat)) {
if (value.startsWith("data:")) {
const resp = await fetch(value);
items[type] = await resp.blob();
} else {
items[type] = new Blob([value], { type });
}
}
await navigator.clipboard.write([new ClipboardItem(items)]);
}, data);
}
async function paste(page) {
await waitForEvent({
page,
eventName: "paste",
action: () => kbPaste(page),
});
}
async function pasteFromClipboard(page, selector = null) {
const validator = e => e.clipboardData.items.length !== 0;
await waitForEvent({
page,
eventName: "paste",
action: () => kbPaste(page),
selector,
validator,
});
}
async function getSerialized(page, filter = undefined) {
const values = await page.evaluate(() => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
if (!map) {
return [];
}
const vals = Array.from(map.values());
for (const value of vals) {
for (const [k, v] of Object.entries(value)) {
// Puppeteer don't serialize typed array correctly, so we convert them
// to arrays.
if (ArrayBuffer.isView(v)) {
value[k] = Array.from(v);
}
}
}
return vals;
});
return filter ? values.map(filter) : values;
}
async function getFirstSerialized(page, filter = undefined) {
return (await getSerialized(page, filter))[0];
}
function getAnnotationStorage(page) {
return page.evaluate(() =>
Object.fromEntries(
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable
.map || []
)
);
}
function waitForEntryInStorage(page, key, value, checker = (x, y) => x === y) {
return page.waitForFunction(
(k, v, c) => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
// eslint-disable-next-line no-eval
return map && eval(`(${c})`)(JSON.stringify(map.get(k)), v);
},
{},
key,
JSON.stringify(value),
checker.toString()
);
}
function getEditors(page, kind) {
return page.evaluate(aKind => {
const elements = document.querySelectorAll(`.${aKind}Editor`);
const results = [];
for (const { id } of elements) {
results.push(parseInt(id.split("_").at(-1)));
}
results.sort();
return results;
}, kind);
}
function getEditorDimensions(page, selector) {
return page.evaluate(sel => {
const { style } = document.querySelector(sel);
return {
left: style.left,
top: style.top,
width: style.width,
height: style.height,
};
}, selector);
}
async function serializeBitmapDimensions(page) {
await page.waitForFunction(() => {
try {
const map =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable
.map;
return !!map;
} catch {
return false;
}
});
return page.evaluate(() => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
return map
? Array.from(map.values(), x => ({
width: x.bitmap.width,
height: x.bitmap.height,
}))
: [];
});
}
async function dragAndDrop(page, selector, translations, steps = 1) {
const rect = await getRect(page, selector);
const startX = rect.x + rect.width / 2;
const startY = rect.y + rect.height / 2;
await page.mouse.move(startX, startY);
await page.mouse.down();
for (const [tX, tY] of translations) {
await page.mouse.move(startX + tX, startY + tY, { steps });
}
await page.mouse.up();
await page.waitForSelector("#viewer:not(.noUserSelect)");
}
function waitForPageChanging(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on("pagechanging", resolve, {
once: true,
});
});
}
function waitForAnnotationEditorLayer(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"annotationeditorlayerrendered",
resolve,
{ once: true }
);
});
}
function waitForAnnotationModeChanged(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"annotationeditormodechanged",
resolve,
{ once: true }
);
});
}
function waitForPageRendered(page, pageNumber) {
return page.evaluateHandle(
number => [
new Promise(resolve => {
const { eventBus } = window.PDFViewerApplication;
eventBus.on("pagerendered", function handler(e) {
if (
!e.isDetailView &&
(number === undefined || e.pageNumber === number)
) {
resolve();
eventBus.off("pagerendered", handler);
}
});
}),
],
pageNumber
);
}
function waitForEditorMovedInDOM(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on("editormovedindom", resolve, {
once: true,
});
});
}
async function scrollIntoView(page, selector) {
const handle = await page.evaluateHandle(
sel => [
new Promise(resolve => {
const container = document.getElementById("viewerContainer");
if (container.scrollHeight <= container.clientHeight) {
resolve();
return;
}
container.addEventListener("scrollend", resolve, { once: true });
const element = document.querySelector(sel);
element.scrollIntoView({ behavior: "instant", block: "start" });
}),
],
selector
);
return awaitPromise(handle);
}
async function firstPageOnTop(page) {
const handle = await page.evaluateHandle(() => [
new Promise(resolve => {
const container = document.getElementById("viewerContainer");
if (container.scrollTop === 0 && container.scrollLeft === 0) {
resolve();
return;
}
container.addEventListener("scrollend", resolve, { once: true });
container.scrollTo(0, 0);
}),
]);
return awaitPromise(handle);
}
async function setCaretAt(page, pageNumber, text, position) {
await page.evaluate(
(pageN, string, pos) => {
for (const el of document.querySelectorAll(
`.page[data-page-number="${pageN}"] > .textLayer > span`
)) {
if (el.textContent === string) {
window.getSelection().setPosition(el.firstChild, pos);
break;
}
}
},
pageNumber,
text,
position
);
}
const modifier = isMac ? "Meta" : "Control";
async function kbCopy(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("c", { commands: ["Copy"] });
await page.keyboard.up(modifier);
}
async function kbPaste(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("v", { commands: ["Paste"] });
await page.keyboard.up(modifier);
}
async function kbUndo(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("z");
await page.keyboard.up(modifier);
}
async function kbRedo(page) {
if (isMac) {
await page.keyboard.down("Meta");
await page.keyboard.down("Shift");
await page.keyboard.press("z");
await page.keyboard.up("Shift");
await page.keyboard.up("Meta");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("y");
await page.keyboard.up("Control");
}
}
async function kbSelectAll(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("a", { commands: ["SelectAll"] });
await page.keyboard.up(modifier);
}
async function kbModifierDown(page) {
await page.keyboard.down(modifier);
}
async function kbModifierUp(page) {
await page.keyboard.up(modifier);
}
async function kbGoToEnd(page) {
if (isMac) {
await page.keyboard.down("Meta");
await page.keyboard.press("ArrowDown", {
commands: ["MoveToEndOfDocument"],
});
await page.keyboard.up("Meta");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("End");
await page.keyboard.up("Control");
}
}
async function kbGoToBegin(page) {
if (isMac) {
await page.keyboard.down("Meta");
await page.keyboard.press("ArrowUp", {
commands: ["MoveToBeginningOfDocument"],
});
await page.keyboard.up("Meta");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("Home");
await page.keyboard.up("Control");
}
}
async function kbBigMoveLeft(page) {
if (isMac) {
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowLeft");
await page.keyboard.up("Shift");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("ArrowLeft");
await page.keyboard.up("Control");
}
}
async function kbBigMoveRight(page) {
if (isMac) {
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowRight");
await page.keyboard.up("Shift");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("ArrowRight");
await page.keyboard.up("Control");
}
}
async function kbBigMoveUp(page) {
if (isMac) {
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Shift");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("ArrowUp");
await page.keyboard.up("Control");
}
}
async function kbBigMoveDown(page) {
if (isMac) {
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Shift");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("ArrowDown");
await page.keyboard.up("Control");
}
}
async function kbDeleteLastWord(page) {
if (isMac) {
await page.keyboard.down("Alt");
await page.keyboard.press("Backspace");
await page.keyboard.up("Alt");
} else {
await page.keyboard.down("Control");
await page.keyboard.press("Backspace");
await page.keyboard.up("Control");
}
}
async function kbFocusNext(page) {
const handle = await createPromise(page, resolve => {
window.addEventListener("focusin", resolve, { once: true });
});
await page.keyboard.press("Tab");
await awaitPromise(handle);
}
async function kbFocusPrevious(page) {
const handle = await createPromise(page, resolve => {
window.addEventListener("focusin", resolve, { once: true });
});
await page.keyboard.down("Shift");
await page.keyboard.press("Tab");
await page.keyboard.up("Shift");
await awaitPromise(handle);
}
async function kbSave(page) {
await page.keyboard.down(modifier);
await page.keyboard.press("s");
await page.keyboard.up(modifier);
}
async function switchToEditor(name, page, disable = false) {
const modeChangedHandle = await createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"annotationeditormodechanged",
resolve,
{ once: true }
);
});
await page.click(`#editor${name}Button`);
name = name.toLowerCase();
await page.waitForSelector(
".annotationEditorLayer" +
(disable ? `:not(.${name}Editing)` : `.${name}Editing`)
);
await awaitPromise(modeChangedHandle);
}
async function selectEditors(name, page) {
await kbSelectAll(page);
await page.waitForFunction(
() => !document.querySelector(`.${name}Editor:not(.selectedEditor)`)
);
}
async function clearEditors(name, page) {
await selectEditors(name, page);
await page.keyboard.press("Backspace");
await waitForStorageEntries(page, 0);
}
function waitForNoElement(page, selector) {
return page.waitForFunction(
sel => !document.querySelector(sel),
{},
selector
);
}
function isCanvasMonochrome(page, pageNumber, rectangle, color) {
return page.evaluate(
(rect, pageN, col) => {
const canvas = document.querySelector(
`.page[data-page-number = "${pageN}"] .canvasWrapper canvas`
);
const canvasRect = canvas.getBoundingClientRect();
const ctx = canvas.getContext("2d");
rect ||= canvasRect;
const { data } = ctx.getImageData(
rect.x - canvasRect.x,
rect.y - canvasRect.y,
rect.width,
rect.height
);
return new Uint32Array(data.buffer).every(x => x === col);
},
rectangle,
pageNumber,
color
);
}
async function getXY(page, selector) {
const rect = await getRect(page, selector);
return `${rect.x}::${rect.y}`;
}
function waitForPositionChange(page, selector, xy) {
return page.waitForFunction(
(sel, currentXY) => {
const bbox = document.querySelector(sel).getBoundingClientRect();
return `${bbox.x}::${bbox.y}` !== currentXY;
},
{},
selector,
xy
);
}
async function moveEditor(page, selector, n, pressKey) {
let xy = await getXY(page, selector);
for (let i = 0; i < n; i++) {
const handle = await waitForEditorMovedInDOM(page);
await pressKey();
await awaitPromise(handle);
await waitForPositionChange(page, selector, xy);
xy = await getXY(page, selector);
}
}
// Unicode bidi isolation characters, Fluent adds these markers to the text.
const FSI = "\u2068";
const PDI = "\u2069";
export {
applyFunctionToEditor,
awaitPromise,
clearEditors,
clearInput,
closePages,
closeSinglePage,
copy,
copyToClipboard,
createPromise,
dragAndDrop,
firstPageOnTop,
FSI,
getAnnotationSelector,
getAnnotationStorage,
getComputedStyleSelector,
getEditorDimensions,
getEditors,
getEditorSelector,
getFirstSerialized,
getQuerySelector,
getRect,
getSelector,
getSerialized,
getSpanRectFromText,
getXY,
isCanvasMonochrome,
kbBigMoveDown,
kbBigMoveLeft,
kbBigMoveRight,
kbBigMoveUp,
kbDeleteLastWord,
kbFocusNext,
kbFocusPrevious,
kbGoToBegin,
kbGoToEnd,
kbModifierDown,
kbModifierUp,
kbRedo,
kbSave,
kbSelectAll,
kbUndo,
loadAndWait,
mockClipboard,
moveEditor,
paste,
pasteFromClipboard,
PDI,
scrollIntoView,
selectEditor,
selectEditors,
serializeBitmapDimensions,
setCaretAt,
switchToEditor,
unselectEditor,
waitAndClick,
waitForAnnotationEditorLayer,
waitForAnnotationModeChanged,
waitForEntryInStorage,
waitForEvent,
waitForNoElement,
waitForPageChanging,
waitForPageRendered,
waitForPointerUp,
waitForSandboxTrip,
waitForSelectedEditor,
waitForSerialized,
waitForStorageEntries,
waitForTimeout,
waitForUnselectedEditor,
};

View File

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

View File

@@ -0,0 +1,571 @@
/* 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,
closeSinglePage,
getSpanRectFromText,
loadAndWait,
waitForEvent,
} from "./test_utils.mjs";
import { startBrowser } from "../test.mjs";
describe("Text layer", () => {
describe("Text selection", () => {
// page.mouse.move(x, y, { steps: ... }) doesn't work in Firefox, because
// puppeteer will send fractional intermediate positions and Firefox doesn't
// support them. Use this function to round each intermediate position to an
// integer.
async function moveInSteps(page, from, to, steps) {
const deltaX = to.x - from.x;
const deltaY = to.y - from.y;
for (let i = 0; i <= steps; i++) {
const x = Math.round(from.x + (deltaX * i) / steps);
const y = Math.round(from.y + (deltaY * i) / steps);
await page.mouse.move(x, y);
}
}
function middlePosition(rect) {
return {
x: Math.round(rect.x + rect.width / 2),
y: Math.round(rect.y + rect.height / 2),
};
}
function middleLeftPosition(rect) {
return {
x: Math.round(rect.x + 1),
y: Math.round(rect.y + rect.height / 2),
};
}
function belowEndPosition(rect) {
return {
x: Math.round(rect.x + rect.width),
y: Math.round(rect.y + rect.height * 1.5),
};
}
beforeEach(() => {
jasmine.addAsyncMatchers({
// Check that a page has a selection containing the given text, with
// some tolerance for extra characters before/after.
toHaveRoughlySelected({ pp }) {
return {
async compare(page, expected) {
const TOLERANCE = 10;
const actual = await page.evaluate(() =>
// We need to normalize EOL for Windows
window.getSelection().toString().replaceAll("\r\n", "\n")
);
let start, end;
if (expected instanceof RegExp) {
const match = expected.exec(actual);
start = -1;
if (match) {
start = match.index;
end = start + match[0].length;
}
} else {
start = actual.indexOf(expected);
if (start !== -1) {
end = start + expected.length;
}
}
const pass =
start !== -1 &&
start < TOLERANCE &&
end > actual.length - TOLERANCE;
return {
pass,
message: `Expected ${pp(
actual.length > 200
? actual.slice(0, 100) + "[...]" + actual.slice(-100)
: actual
)} to ${pass ? "not " : ""}roughly match ${pp(expected)}.`,
};
},
};
},
});
});
describe("using mouse", () => {
describe("doesn't jump when hovering on an empty area", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
afterEach(async () => {
await closePages(pages);
});
it("in a single page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"(frequently executed) bytecode sequences, records"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"them, and compiles them to fast native code. We call such a se-"
).then(belowEndPosition),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected(
"code sequences, records\n" +
"them, and compiles them to fast native code. We call suc"
);
})
);
});
it("across multiple pages", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const scrollTarget = await getSpanRectFromText(
page,
1,
"Unlike method-based dynamic compilers, our dynamic com-"
);
await page.evaluate(top => {
document.getElementById("viewerContainer").scrollTop = top;
}, scrollTarget.y - 50);
const [
positionStartPage1,
positionEndPage1,
positionStartPage2,
positionEndPage2,
] = await Promise.all([
getSpanRectFromText(
page,
1,
"Each compiled trace covers one path through the program with"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"or that the same types will occur in subsequent loop iterations."
).then(middlePosition),
getSpanRectFromText(
page,
2,
"Hence, recording and compiling a trace"
).then(middlePosition),
getSpanRectFromText(
page,
2,
"cache. Alternatively, the VM could simply stop tracing, and give up"
).then(belowEndPosition),
]);
await page.mouse.move(positionStartPage1.x, positionStartPage1.y);
await page.mouse.down();
await moveInSteps(page, positionStartPage1, positionEndPage1, 20);
await moveInSteps(page, positionEndPage1, positionStartPage2, 20);
await expectAsync(page)
.withContext(`In ${browserName}, first selection`)
.toHaveRoughlySelected(
/path through the program .*Hence, recording a/s
);
await moveInSteps(page, positionStartPage2, positionEndPage2, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}, second selection`)
.toHaveRoughlySelected(
/path through.*Hence, recording and .* tracing, and give/s
);
})
);
});
});
describe("doesn't jump when hovering on an empty area, with .markedContent", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"chrome-text-selection-markedContent.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
afterEach(async () => {
await closePages(pages);
});
it("in per-character selection mode", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"strengthen in the coming quarters as the railway projects under"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"development enter the construction phase (estimated at around"
).then(belowEndPosition),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected(
"rs as the railway projects under\n" +
"development enter the construction phase (estimated at "
);
})
);
});
it("in per-word selection mode", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"strengthen in the coming quarters as the railway projects under"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"development enter the construction phase (estimated at around"
).then(belowEndPosition),
]);
// Puppeteer doesn't support emulating "double click and hold" for
// WebDriver BiDi, so we must manually dispatch a protocol action
// (see https://github.com/puppeteer/puppeteer/issues/13745).
await page.mainFrame().browsingContext.performActions([
{
type: "pointer",
id: "__puppeteer_mouse",
actions: [
{ type: "pointerMove", ...positionStart },
{ type: "pointerDown", button: 0 },
{ type: "pointerUp", button: 0 },
{ type: "pointerDown", button: 0 },
],
},
]);
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected(
"quarters as the railway projects under\n" +
"development enter the construction phase (estimated at around"
);
})
);
});
});
describe("when selecting over a link", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"annotation-link-text-popup.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
afterEach(async () => {
await closePages(pages);
});
it("allows selecting within the link", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected("Link\nmozil");
})
);
});
it("allows selecting within the link when going backwards", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Text").then(middlePosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`In ${browserName}`)
.toHaveRoughlySelected("a.org\nTe");
})
);
});
it("allows clicking the link after selecting", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await waitForEvent({
page,
eventName: "click",
action: () => page.mouse.click(positionEnd.x, positionEnd.y),
selector: "#pdfjs_internal_id_8R",
validator: e => {
// Don't navigate to the link destination: the `click` event
// firing is enough validation that the link can be clicked.
e.preventDefault();
return true;
},
});
})
);
});
it("allows clicking the link after changing selection with the keyboard", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [positionStart, positionEnd] = await Promise.all([
getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
getSpanRectFromText(page, 1, "mozilla.org").then(
middlePosition
),
]);
await page.mouse.move(positionStart.x, positionStart.y);
await page.mouse.down();
await moveInSteps(page, positionStart, positionEnd, 20);
await page.mouse.up();
await page.keyboard.down("Shift");
await page.keyboard.press("ArrowRight");
await page.keyboard.up("Shift");
await waitForEvent({
page,
eventName: "click",
action: () => page.mouse.click(positionEnd.x, positionEnd.y),
selector: "#pdfjs_internal_id_8R",
validator: e => {
// Don't navigate to the link destination: the `click` event
// firing is enough validation that the link can be clicked.
e.preventDefault();
return true;
},
});
})
);
});
});
});
describe("using selection carets", () => {
let browser;
let page;
beforeEach(async () => {
// Chrome does not support simulating caret-based selection, so this
// test only runs in Firefox.
browser = await startBrowser({
browserName: "firefox",
startUrl: "",
extraPrefsFirefox: {
"layout.accessiblecaret.enabled": true,
"layout.accessiblecaret.hide_carets_for_mouse_input": false,
},
});
page = await browser.newPage();
await page.goto(
`${global.integrationBaseUrl}?file=/test/pdfs/tracemonkey.pdf#zoom=page-fit`
);
await page.bringToFront();
await page.waitForSelector(
`.page[data-page-number = "1"] .endOfContent`,
{ timeout: 0 }
);
});
afterEach(async () => {
await closeSinglePage(page);
await browser.close();
});
it("doesn't jump when moving selection", async () => {
const [initialStart, initialEnd, finalEnd] = await Promise.all([
getSpanRectFromText(
page,
1,
"(frequently executed) bytecode sequences, records"
).then(middleLeftPosition),
getSpanRectFromText(
page,
1,
"(frequently executed) bytecode sequences, records"
).then(middlePosition),
getSpanRectFromText(
page,
1,
"them, and compiles them to fast native code. We call such a se-"
).then(belowEndPosition),
]);
await page.mouse.move(initialStart.x, initialStart.y);
await page.mouse.down();
await moveInSteps(page, initialStart, initialEnd, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`first selection`)
.toHaveRoughlySelected("frequently executed) byt");
const initialCaretPos = {
x: initialEnd.x,
y: initialEnd.y + 10,
};
const intermediateCaretPos = {
x: finalEnd.x,
y: finalEnd.y + 5,
};
const finalCaretPos = {
x: finalEnd.x + 20,
y: finalEnd.y + 5,
};
await page.mouse.move(initialCaretPos.x, initialCaretPos.y);
await page.mouse.down();
await moveInSteps(page, initialCaretPos, intermediateCaretPos, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`second selection`)
.toHaveRoughlySelected(/frequently .* We call such a s/s);
await page.mouse.down();
await moveInSteps(page, intermediateCaretPos, finalCaretPos, 20);
await page.mouse.up();
await expectAsync(page)
.withContext(`third selection`)
.toHaveRoughlySelected(/frequently .* We call such a s/s);
});
});
});
describe("when the browser enforces a minimum font size", () => {
let browser;
let page;
beforeEach(async () => {
// Only testing in Firefox because, while Chrome has a setting similar to
// font.minimum-size.x-western, it is not exposed through its API.
browser = await startBrowser({
browserName: "firefox",
startUrl: "",
extraPrefsFirefox: { "font.minimum-size.x-western": 40 },
});
page = await browser.newPage();
await page.goto(
`${global.integrationBaseUrl}?file=/test/pdfs/tracemonkey.pdf#zoom=100`
);
await page.bringToFront();
await page.waitForSelector(
`.page[data-page-number = "1"] .endOfContent`,
{ timeout: 0 }
);
});
afterEach(async () => {
await closeSinglePage(page);
await browser.close();
});
it("renders spans with the right size", async () => {
const rect = await getSpanRectFromText(
page,
1,
"Dynamic languages such as JavaScript are more difficult to com-"
);
// The difference between `a` and `b`, as a percentage of the lower one
const getPercentDiff = (a, b) => Math.max(a, b) / Math.min(a, b) - 1;
expect(getPercentDiff(rect.width, 315)).toBeLessThan(0.03);
expect(getPercentDiff(rect.height, 12)).toBeLessThan(0.03);
});
});
});

View File

@@ -0,0 +1,35 @@
import { closePages, loadAndWait } from "./test_utils.mjs";
describe("PDF Thumbnail View", () => {
describe("Works without errors", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", "#sidebarToggleButton");
});
afterEach(async () => {
await closePages(pages);
});
it("should render thumbnails without errors", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#sidebarToggleButton");
const thumbSelector = "#thumbnailView .thumbnailImage";
await page.waitForSelector(thumbSelector, { visible: true });
await page.waitForSelector(
'#thumbnailView .thumbnail[data-loaded="true"]'
);
const src = await page.$eval(thumbSelector, el => el.src);
expect(src)
.withContext(`In ${browserName}`)
.toMatch(/^data:image\//);
})
);
});
});
});

File diff suppressed because it is too large Load Diff