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
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:
305
test/integration/accessibility_spec.mjs
Normal file
305
test/integration/accessibility_spec.mjs
Normal 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("");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
881
test/integration/annotation_spec.mjs
Normal file
881
test/integration/annotation_spec.mjs
Normal 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;
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
262
test/integration/autolinker_spec.mjs
Normal file
262
test/integration/autolinker_spec.mjs
Normal file
@@ -0,0 +1,262 @@
|
||||
/* Copyright 2025 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
awaitPromise,
|
||||
closePages,
|
||||
createPromise,
|
||||
loadAndWait,
|
||||
} from "./test_utils.mjs";
|
||||
|
||||
function waitForLinkAnnotations(page, pageNumber) {
|
||||
return page.evaluateHandle(
|
||||
number => [
|
||||
new Promise(resolve => {
|
||||
const { eventBus } = window.PDFViewerApplication;
|
||||
eventBus.on("linkannotationsadded", function listener(e) {
|
||||
if (number === undefined || e.pageNumber === number) {
|
||||
resolve();
|
||||
eventBus.off("linkannotationsadded", listener);
|
||||
}
|
||||
});
|
||||
}),
|
||||
],
|
||||
pageNumber
|
||||
);
|
||||
}
|
||||
|
||||
function recordInitialLinkAnnotationsEvent(eventBus) {
|
||||
globalThis.initialLinkAnnotationsEventFired = false;
|
||||
eventBus.on(
|
||||
"linkannotationsadded",
|
||||
() => {
|
||||
globalThis.initialLinkAnnotationsEventFired = true;
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
}
|
||||
function waitForInitialLinkAnnotations(page) {
|
||||
return createPromise(page, resolve => {
|
||||
if (globalThis.initialLinkAnnotationsEventFired) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
window.PDFViewerApplication.eventBus.on("linkannotationsadded", resolve, {
|
||||
once: true,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
describe("autolinker", function () {
|
||||
describe("bug1019475_2.pdf", function () {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"bug1019475_2.pdf",
|
||||
".annotationLayer",
|
||||
null,
|
||||
null,
|
||||
{
|
||||
enableAutoLinking: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must appropriately add link annotations when relevant", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForLinkAnnotations(page);
|
||||
const url = await page.$$eval(
|
||||
".annotationLayer > .linkAnnotation > a",
|
||||
annotations => annotations.map(a => a.href)
|
||||
);
|
||||
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
|
||||
expect(url[0])
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual("http://www.mozilla.org/");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("bug1019475_1.pdf", function () {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"bug1019475_1.pdf",
|
||||
".annotationLayer",
|
||||
null,
|
||||
null,
|
||||
{
|
||||
enableAutoLinking: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must not add links when unnecessary", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForLinkAnnotations(page);
|
||||
const linkIds = await page.$$eval(
|
||||
".annotationLayer > .linkAnnotation > a",
|
||||
annotations =>
|
||||
annotations.map(a => a.getAttribute("data-element-id"))
|
||||
);
|
||||
expect(linkIds.length).withContext(`In ${browserName}`).toEqual(3);
|
||||
linkIds.forEach(id =>
|
||||
expect(id)
|
||||
.withContext(`In ${browserName}`)
|
||||
.not.toContain("inferred_link_")
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pr19449.pdf", function () {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait("pr19449.pdf", ".annotationLayer", null, null, {
|
||||
docBaseUrl: "http://example.com",
|
||||
enableAutoLinking: true,
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must not add links that overlap even if the URLs are different", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForLinkAnnotations(page);
|
||||
const linkIds = await page.$$eval(
|
||||
".annotationLayer > .linkAnnotation > a",
|
||||
annotations =>
|
||||
annotations.map(a => a.getAttribute("data-element-id"))
|
||||
);
|
||||
expect(linkIds.length).withContext(`In ${browserName}`).toEqual(1);
|
||||
linkIds.forEach(id =>
|
||||
expect(id)
|
||||
.withContext(`In ${browserName}`)
|
||||
.not.toContain("inferred_link_")
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("PR 19470", function () {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"bug1019475_2.pdf",
|
||||
".annotationLayer",
|
||||
null,
|
||||
null,
|
||||
{
|
||||
enableAutoLinking: true,
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must not repeatedly add link annotations redundantly", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await waitForLinkAnnotations(page);
|
||||
let url = await page.$$eval(
|
||||
".annotationLayer > .linkAnnotation > a",
|
||||
annotations => annotations.map(a => a.href)
|
||||
);
|
||||
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
|
||||
|
||||
await page.evaluate(() =>
|
||||
window.PDFViewerApplication.pdfViewer.updateScale({
|
||||
drawingDelay: -1,
|
||||
scaleFactor: 2,
|
||||
})
|
||||
);
|
||||
await waitForLinkAnnotations(page);
|
||||
url = await page.$$eval(
|
||||
".annotationLayer > .linkAnnotation > a",
|
||||
annotations => annotations.map(a => a.href)
|
||||
);
|
||||
expect(url.length).withContext(`In ${browserName}`).toEqual(1);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when highlighting search results", function () {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"issue3115r.pdf",
|
||||
".annotationLayer",
|
||||
null,
|
||||
{ eventBusSetup: recordInitialLinkAnnotationsEvent },
|
||||
{ enableAutoLinking: true }
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must find links that overlap with search results", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await awaitPromise(await waitForInitialLinkAnnotations(page));
|
||||
|
||||
const linkAnnotationsPromise = await waitForLinkAnnotations(page, 36);
|
||||
|
||||
// Search for "rich.edu"
|
||||
await page.click("#viewFindButton");
|
||||
await page.waitForSelector("#viewFindButton", { hidden: false });
|
||||
await page.type("#findInput", "rich.edu");
|
||||
await page.waitForSelector(".textLayer .highlight");
|
||||
|
||||
await awaitPromise(linkAnnotationsPromise);
|
||||
|
||||
const urls = await page.$$eval(
|
||||
".page[data-page-number='36'] > .annotationLayer > .linkAnnotation > a",
|
||||
annotations => annotations.map(a => a.href)
|
||||
);
|
||||
|
||||
expect(urls)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toContain(jasmine.stringContaining("rich.edu"));
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
97
test/integration/caret_browsing_spec.mjs
Normal file
97
test/integration/caret_browsing_spec.mjs
Normal 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");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
627
test/integration/comment_spec.mjs
Normal file
627
test/integration/comment_spec.mjs
Normal 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 });
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
181
test/integration/copy_paste_spec.mjs
Normal file
181
test/integration/copy_paste_spec.mjs
Normal file
@@ -0,0 +1,181 @@
|
||||
/* Copyright 2023 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
closePages,
|
||||
copy,
|
||||
kbSelectAll,
|
||||
loadAndWait,
|
||||
mockClipboard,
|
||||
waitForEvent,
|
||||
} from "./test_utils.mjs";
|
||||
|
||||
const selectAll = async page => {
|
||||
await waitForEvent({
|
||||
page,
|
||||
eventName: "selectionchange",
|
||||
action: () => kbSelectAll(page),
|
||||
});
|
||||
|
||||
await page.waitForFunction(() => {
|
||||
const selection = document.getSelection();
|
||||
const hiddenCopyElement = document.getElementById("hiddenCopyElement");
|
||||
return selection.containsNode(hiddenCopyElement);
|
||||
});
|
||||
};
|
||||
|
||||
describe("Copy and paste", () => {
|
||||
describe("all text", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait("tracemonkey.pdf", "#hiddenCopyElement", 100);
|
||||
await mockClipboard(pages);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must check that we've all the contents on copy/paste", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await page.waitForSelector(
|
||||
".page[data-page-number='1'] .textLayer .endOfContent"
|
||||
);
|
||||
await selectAll(page);
|
||||
|
||||
await copy(page);
|
||||
await page.waitForFunction(
|
||||
`document.querySelector('#viewerContainer').style.cursor !== "wait"`
|
||||
);
|
||||
|
||||
await page.waitForFunction(
|
||||
async () =>
|
||||
!!(await navigator.clipboard.readText())?.includes(
|
||||
"Dynamic languages such as JavaScript"
|
||||
)
|
||||
);
|
||||
const text = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
|
||||
expect(
|
||||
text.includes("This section provides an overview of our system")
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(
|
||||
text.includes(
|
||||
"are represented by function calls. This makes the LIR used by"
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(
|
||||
text.includes("When compiling loops, we consult the oracle before")
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(text.includes("Nested Trace Tree Formation"))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(
|
||||
text.includes(
|
||||
"An important detail is that the call to the inner trace"
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(text.includes("When trace recording is completed, nanojit"))
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(
|
||||
text.includes(
|
||||
"SpiderMonkey, like many VMs, needs to preempt the user program"
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(
|
||||
text.includes(
|
||||
"Using similar computations, we find that trace recording takes"
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(
|
||||
text.includes(
|
||||
"specialization algorithm. We also described our trace compiler"
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
expect(
|
||||
text.includes(
|
||||
"dynamic optimization system. In Proceedings of the ACM SIGPLAN"
|
||||
)
|
||||
)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual(true);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
describe("Copy/paste and ligatures", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait(
|
||||
"copy_paste_ligatures.pdf",
|
||||
"#hiddenCopyElement",
|
||||
100
|
||||
);
|
||||
await mockClipboard(pages);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must check that the ligatures have been removed when the text has been copied", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await page.waitForSelector(
|
||||
".page[data-page-number='1'] .textLayer .endOfContent"
|
||||
);
|
||||
await selectAll(page);
|
||||
|
||||
await copy(page);
|
||||
await page.waitForFunction(
|
||||
`document.querySelector('#viewerContainer').style.cursor !== "wait"`
|
||||
);
|
||||
|
||||
await page.waitForFunction(
|
||||
async () => !!(await navigator.clipboard.readText())
|
||||
);
|
||||
|
||||
const text = await page.evaluate(() =>
|
||||
navigator.clipboard.readText()
|
||||
);
|
||||
|
||||
expect(text)
|
||||
.withContext(`In ${browserName}`)
|
||||
.toEqual("abcdeffffiflffifflſtstghijklmno");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
97
test/integration/document_properties_spec.mjs
Normal file
97
test/integration/document_properties_spec.mjs
Normal 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",
|
||||
});
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
180
test/integration/find_spec.mjs
Normal file
180
test/integration/find_spec.mjs
Normal 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();
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
3602
test/integration/freetext_editor_spec.mjs
Normal file
3602
test/integration/freetext_editor_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
3027
test/integration/highlight_editor_spec.mjs
Normal file
3027
test/integration/highlight_editor_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
1345
test/integration/ink_editor_spec.mjs
Normal file
1345
test/integration/ink_editor_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
83
test/integration/jasmine-boot.js
Normal file
83
test/integration/jasmine-boot.js
Normal 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 };
|
||||
2649
test/integration/scripting_spec.mjs
Normal file
2649
test/integration/scripting_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
774
test/integration/signature_editor_spec.mjs
Normal file
774
test/integration/signature_editor_spec.mjs
Normal file
@@ -0,0 +1,774 @@
|
||||
/* Copyright 2025 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import {
|
||||
awaitPromise,
|
||||
closePages,
|
||||
copy,
|
||||
FSI,
|
||||
getEditorSelector,
|
||||
getRect,
|
||||
loadAndWait,
|
||||
paste,
|
||||
PDI,
|
||||
switchToEditor,
|
||||
waitForPointerUp,
|
||||
waitForTimeout,
|
||||
} from "./test_utils.mjs";
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import { PNG } from "pngjs";
|
||||
|
||||
const __dirname = import.meta.dirname;
|
||||
|
||||
const switchToSignature = switchToEditor.bind(null, "Signature");
|
||||
|
||||
describe("Signature Editor", () => {
|
||||
const descriptionInputSelector = "#addSignatureDescription > input";
|
||||
const addButtonSelector = "#addSignatureAddButton";
|
||||
|
||||
describe("Basic operations", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must check that the editor has been removed when the dialog is cancelled", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([_, page]) => {
|
||||
await switchToSignature(page);
|
||||
await page.click("#editorSignatureAddSignature");
|
||||
|
||||
await page.waitForSelector("#addSignatureDialog", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
// An invisible editor is created but invisible.
|
||||
const editorSelector = getEditorSelector(0);
|
||||
await page.waitForSelector(editorSelector, { visible: false });
|
||||
|
||||
await page.click("#addSignatureCancelButton");
|
||||
|
||||
// The editor should have been removed.
|
||||
await page.waitForSelector(`:not(${editorSelector})`);
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("must check that the basic and common elements are working as expected", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
await switchToSignature(page);
|
||||
await page.click("#editorSignatureAddSignature");
|
||||
|
||||
await page.waitForSelector("#addSignatureDialog", {
|
||||
visible: true,
|
||||
});
|
||||
|
||||
await page.waitForSelector(
|
||||
"#addSignatureTypeButton[aria-selected=true]"
|
||||
);
|
||||
await page.click("#addSignatureTypeInput");
|
||||
await page.waitForSelector(
|
||||
"#addSignatureSaveContainer > input:disabled"
|
||||
);
|
||||
let description = await page.$eval(
|
||||
descriptionInputSelector,
|
||||
el => el.value
|
||||
);
|
||||
expect(description).withContext(browserName).toEqual("");
|
||||
await page.waitForSelector(`${addButtonSelector}:disabled`);
|
||||
await page.waitForSelector("#addSignatureDescInput:disabled");
|
||||
|
||||
await page.type("#addSignatureTypeInput", "PDF.js");
|
||||
await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
|
||||
await page.waitForSelector("#addSignatureDescInput:not(:disabled)");
|
||||
|
||||
// The save button should be enabled now.
|
||||
await page.waitForSelector(
|
||||
"#addSignatureSaveContainer > input:not(:disabled)"
|
||||
);
|
||||
await page.waitForSelector("#addSignatureSaveCheckbox:checked");
|
||||
|
||||
// The description has been filled in automatically.
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("${descriptionInputSelector}").value !== ""`
|
||||
);
|
||||
description = await page.$eval(
|
||||
descriptionInputSelector,
|
||||
el => el.value
|
||||
);
|
||||
expect(description).withContext(browserName).toEqual("PDF.js");
|
||||
|
||||
// Clear the description.
|
||||
await page.click("#addSignatureDescription > button");
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("${descriptionInputSelector}").value === ""`
|
||||
);
|
||||
|
||||
// Clear the text for the signature.
|
||||
await page.click("#clearSignatureButton");
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("#addSignatureTypeInput").value === ""`
|
||||
);
|
||||
// The save button should be disabled now.
|
||||
await page.waitForSelector(
|
||||
"#addSignatureSaveContainer > input:disabled"
|
||||
);
|
||||
await page.waitForSelector(`${addButtonSelector}:disabled`);
|
||||
|
||||
await page.type("#addSignatureTypeInput", "PDF.js");
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("${descriptionInputSelector}").value !== ""`
|
||||
);
|
||||
|
||||
// Clearing the signature type should clear the description.
|
||||
await page.click("#clearSignatureButton");
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("#addSignatureTypeInput").value === ""`
|
||||
);
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("${descriptionInputSelector}").value === ""`
|
||||
);
|
||||
|
||||
// Add a signature and change the description.
|
||||
await page.type("#addSignatureTypeInput", "PDF.js");
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("${descriptionInputSelector}").value !== ""`
|
||||
);
|
||||
await page.click("#addSignatureDescription > button");
|
||||
await page.waitForFunction(
|
||||
`document.querySelector("${descriptionInputSelector}").value === ""`
|
||||
);
|
||||
await page.type(descriptionInputSelector, "Hello World");
|
||||
await page.type("#addSignatureTypeInput", "Hello");
|
||||
|
||||
// The description mustn't be changed.
|
||||
// eslint-disable-next-line no-restricted-syntax
|
||||
await waitForTimeout(100);
|
||||
description = await page.$eval(
|
||||
descriptionInputSelector,
|
||||
el => el.value
|
||||
);
|
||||
expect(description).withContext(browserName).toEqual("Hello World");
|
||||
|
||||
await page.click("#addSignatureAddButton");
|
||||
await page.waitForSelector("#addSignatureDialog", {
|
||||
visible: false,
|
||||
});
|
||||
|
||||
const editorSelector = getEditorSelector(0);
|
||||
await page.waitForSelector(editorSelector, { visible: true });
|
||||
await page.waitForSelector(
|
||||
`.canvasWrapper > svg use[href="#path_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 });
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
1827
test/integration/stamp_editor_spec.mjs
Normal file
1827
test/integration/stamp_editor_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
964
test/integration/test_utils.mjs
Normal file
964
test/integration/test_utils.mjs
Normal 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,
|
||||
};
|
||||
39
test/integration/text_field_spec.mjs
Normal file
39
test/integration/text_field_spec.mjs
Normal file
@@ -0,0 +1,39 @@
|
||||
/* Copyright 2024 Mozilla Foundation
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { closePages, getSelector, loadAndWait } from "./test_utils.mjs";
|
||||
|
||||
describe("Text field", () => {
|
||||
describe("Empty text field", () => {
|
||||
let pages;
|
||||
|
||||
beforeEach(async () => {
|
||||
pages = await loadAndWait("file_pdfjs_form.pdf", getSelector("7R"));
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await closePages(pages);
|
||||
});
|
||||
|
||||
it("must check that the field is empty although its appearance contains a white space", async () => {
|
||||
await Promise.all(
|
||||
pages.map(async ([browserName, page]) => {
|
||||
const text = await page.$eval(getSelector("7R"), el => el.value);
|
||||
expect(text).withContext(`In ${browserName}`).toEqual("");
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
571
test/integration/text_layer_spec.mjs
Normal file
571
test/integration/text_layer_spec.mjs
Normal 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);
|
||||
});
|
||||
});
|
||||
});
|
||||
35
test/integration/thumbnail_view_spec.mjs
Normal file
35
test/integration/thumbnail_view_spec.mjs
Normal 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\//);
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
1464
test/integration/viewer_spec.mjs
Normal file
1464
test/integration/viewer_spec.mjs
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user