1
0
Fork 0
mirror of https://github.com/mozilla/pdf.js.git synced 2025-04-19 06:38:07 +02:00
pdf.js/test/integration/freetext_editor_spec.mjs

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

3310 lines
108 KiB
JavaScript
Raw Normal View History

/* Copyright 2022 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,
clearEditors,
closePages,
copy,
copyToClipboard,
createPromise,
dragAndDrop,
firstPageOnTop,
getEditors,
getEditorSelector,
getFirstSerialized,
getRect,
getSerialized,
hover,
isCanvasMonochrome,
kbBigMoveDown,
kbBigMoveLeft,
kbBigMoveRight,
kbBigMoveUp,
kbGoToBegin,
kbGoToEnd,
kbModifierDown,
kbModifierUp,
kbRedo,
kbUndo,
loadAndWait,
moveEditor,
paste,
pasteFromClipboard,
scrollIntoView,
selectEditor,
selectEditors,
switchToEditor,
unselectEditor,
waitForAnnotationEditorLayer,
waitForAnnotationModeChanged,
waitForSelectedEditor,
waitForSerialized,
waitForStorageEntries,
waitForTimeout,
waitForUnselectedEditor,
} from "./test_utils.mjs";
import { PNG } from "pngjs";
const selectAll = selectEditors.bind(null, "freeText");
const clearAll = clearEditors.bind(null, "freeText");
const commit = async page => {
await page.keyboard.press("Escape");
await page.waitForSelector(".freeTextEditor.selectedEditor .overlay.enabled");
};
const switchToFreeText = switchToEditor.bind(null, "FreeText");
2023-10-26 22:44:35 +02:00
const cancelFocusIn = async (page, selector) => {
page.evaluate(sel => {
const el = document.querySelector(sel);
el.addEventListener(
"focusin",
evt => {
evt.preventDefault();
evt.stopPropagation();
},
{ capture: true, once: true }
);
}, selector);
};
describe("FreeText Editor", () => {
describe("FreeText", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("aboutstacks.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must write a string in a FreeText editor", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await waitForSelectedEditor(page, editorSelector);
await waitForStorageEntries(page, 1);
let content = await page.$eval(editorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
// Edit again.
await page.keyboard.press("Enter");
await page.waitForSelector(
`${editorSelector} .overlay:not(.enabled)`
);
await commit(page);
content = await page.$eval(editorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
})
);
});
it("must copy/paste", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const firstEditorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(firstEditorSelector, { visible: true });
await page.type(`${firstEditorSelector} .internal`, data);
await commit(page);
await waitForStorageEntries(page, 1);
await selectEditor(page, firstEditorSelector);
await copy(page);
await paste(page);
const secondEditorSelector = getEditorSelector(1);
await page.waitForSelector(secondEditorSelector, { visible: true });
await waitForStorageEntries(page, 2);
const content = await page.$eval(firstEditorSelector, el =>
el.innerText.trimEnd().replaceAll("\xa0", " ")
);
let pastedContent = await page.$eval(secondEditorSelector, el =>
el.innerText.trimEnd().replaceAll("\xa0", " ")
);
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
await copy(page);
await paste(page);
const thirdEditorSelector = getEditorSelector(2);
await page.waitForSelector(thirdEditorSelector, { visible: true });
await waitForStorageEntries(page, 3);
pastedContent = await page.$eval(thirdEditorSelector, el =>
el.innerText.trimEnd().replaceAll("\xa0", " ")
);
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
}
});
it("must clear all", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
for (const n of [0, 1, 2]) {
const editorSelector = getEditorSelector(n);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100 * n, rect.y + 100 * n);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
const hasEditor = await page.evaluate(
sel => !!document.querySelector(sel),
editorSelector
);
expect(hasEditor).withContext(`In ${browserName}`).toEqual(true);
}
await waitForStorageEntries(page, 3);
await clearAll(page);
await waitForStorageEntries(page, 0);
for (const n of [0, 1, 2]) {
const hasEditor = await page.evaluate(
sel => !!document.querySelector(sel),
getEditorSelector(n)
);
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
}
})
);
});
it("must check that a paste has been undone", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
let editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await selectEditor(page, editorSelector);
await copy(page);
await paste(page);
editorSelector = getEditorSelector(1);
await page.waitForSelector(editorSelector, { visible: true });
await kbUndo(page);
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
editorSelector
);
for (let i = 0; i < 2; i++) {
await paste(page);
await page.waitForSelector(getEditorSelector(2 + i));
}
for (let i = 0; i < 2; i++) {
await kbUndo(page);
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
getEditorSelector(3 - i)
);
}
}
});
it("must check that aria-owns is correct", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
2023-10-26 22:44:35 +02:00
await page.$eval(".textLayer", el => {
for (const span of el.querySelectorAll(
`span[role="presentation"]`
)) {
if (span.innerText.includes("Stacks are simple to create")) {
span.setAttribute("pdfjs", true);
}
}
});
await scrollIntoView(page, `span[pdfjs="true"]`);
const stacksRect = await getRect(page, `span[pdfjs="true"]`);
const oldAriaOwns = await page.$eval(`span[pdfjs="true"]`, el =>
el.getAttribute("aria-owns")
);
expect(oldAriaOwns).withContext(`In ${browserName}`).toEqual(null);
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(
stacksRect.x + stacksRect.width + 1,
stacksRect.y + stacksRect.height / 2
);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
const ariaOwns = await page.$eval(".textLayer", el => {
const span = el.querySelector(`span[pdfjs="true"]`);
return span?.getAttribute("aria-owns") || null;
});
expect(ariaOwns.endsWith("_0-editor"))
.withContext(`In ${browserName}`)
.toEqual(true);
2023-10-26 22:44:35 +02:00
await scrollIntoView(page, ".annotationEditorLayer");
})
);
});
it("must check that right click doesn't select", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([0]);
await page.keyboard.press("Escape");
await page.waitForFunction(
() => !document.querySelector(".selectedEditor")
);
await selectEditor(page, editorSelector);
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([0]);
// Escape.
await page.keyboard.press("Escape");
await page.waitForFunction(
() => !document.querySelector(".selectedEditor")
);
// TODO: uncomment that stuff once we've a way to dismiss
// the context menu.
/* await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
{ button: "right" }
); */
})
);
});
it("must check that text change can be undone/redone", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
await page.mouse.click(rect.x + 200, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
for (let i = 0; i < 5; i++) {
await page.type(`${editorSelector} .internal`, "A");
await commit(page);
if (i < 4) {
await selectEditor(page, editorSelector, /* count = */ 2);
}
}
let prevText = await page.$eval(
`${editorSelector} .internal`,
el => el.innerText
);
const waitForTextChange = previous =>
page.waitForFunction(
(prev, sel) => document.querySelector(sel).innerText !== prev,
{},
previous,
`${editorSelector} .internal`
);
const getText = () =>
page.$eval(`${editorSelector} .internal`, el => el.innerText);
// We're in the middle of the text.
await kbUndo(page);
await waitForTextChange(prevText);
let text = (prevText = await getText());
expect(text).withContext(`In ${browserName}`).toEqual("AAAA");
await kbUndo(page);
await waitForTextChange(prevText);
text = prevText = await getText();
expect(text).withContext(`In ${browserName}`).toEqual("AAA");
await kbRedo(page);
await waitForTextChange(prevText);
text = prevText = await getText();
expect(text).withContext(`In ${browserName}`).toEqual("AAAA");
for (let i = 0; i < 4; i++) {
await kbUndo(page);
if (i < 3) {
await waitForTextChange(prevText);
prevText = await getText();
}
}
await page.waitForFunction(
() => !document.querySelector(".selectedEditor")
);
await kbRedo(page);
await page.waitForSelector(editorSelector, { visible: true });
text = await getText();
expect(text).withContext(`In ${browserName}`).toEqual("A");
// Add a new A.
await selectEditor(page, editorSelector, /* count = */ 2);
await page.type(`${editorSelector} .internal`, "A");
await commit(page);
text = await getText();
expect(text).withContext(`In ${browserName}`).toEqual("AA");
}
});
});
describe("FreeText (multiselection)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("aboutstacks.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must select/unselect several editors and check copy, paste and delete operations", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorCenters = [];
let lastX = rect.x + rect.width / 10;
for (let i = 0; i < 4; i++) {
const editorSelector = getEditorSelector(i);
const data = `FreeText ${i}`;
await page.mouse.click(lastX, rect.y + rect.height / 10);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
const editorRect = await getRect(page, editorSelector);
lastX = editorRect.x + editorRect.width + 10;
editorCenters.push({
x: editorRect.x + editorRect.width / 2,
y: editorRect.y + editorRect.height / 2,
});
}
await selectAll(page);
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([0, 1, 2, 3]);
// Unselect the editor.
await kbModifierDown(page);
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
await waitForUnselectedEditor(page, getEditorSelector(1));
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([0, 2, 3]);
await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
await waitForUnselectedEditor(page, getEditorSelector(2));
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([0, 3]);
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
await kbModifierUp(page);
await waitForSelectedEditor(page, getEditorSelector(1));
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([0, 1, 3]);
await copy(page);
await paste(page);
await page.waitForSelector(getEditorSelector(6), { visible: true });
// 0,1,3 are unselected and new pasted editors are selected.
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([4, 5, 6]);
// No ctrl here, hence all are unselected and 2 is selected.
await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
await waitForSelectedEditor(page, getEditorSelector(2));
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([2]);
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
await waitForSelectedEditor(page, getEditorSelector(1));
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([1]);
await kbModifierDown(page);
await page.mouse.click(editorCenters[3].x, editorCenters[3].y);
await waitForSelectedEditor(page, getEditorSelector(3));
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([1, 3]);
await kbModifierUp(page);
// Delete 1 and 3.
await page.keyboard.press("Backspace");
await page.waitForFunction(
sels => sels.every(sel => !document.querySelector(sel)),
{},
[1, 3].map(getEditorSelector)
);
await selectAll(page);
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([0, 2, 4, 5, 6]);
// Create an empty editor.
await page.mouse.click(
rect.x + (rect.width / 10) * 7,
rect.y + rect.height / 10
);
await page.waitForSelector(getEditorSelector(7), { visible: true });
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([7]);
// Set the focus to 2 and check that only 2 is selected.
await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
await waitForSelectedEditor(page, getEditorSelector(2));
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([2]);
// Create an empty editor.
await page.mouse.click(
rect.x + (rect.width / 10) * 8,
rect.y + rect.height / 10
);
await page.waitForSelector(getEditorSelector(8), { visible: true });
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([8]);
// Dismiss it.
await page.keyboard.press("Escape");
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
getEditorSelector(8)
);
await selectAll(page);
// Check that all the editors are correctly selected (and the focus
// didn't move to the body when the empty editor was removed).
expect(await getEditors(page, "selected"))
.withContext(`In ${browserName}`)
.toEqual([0, 2, 4, 5, 6]);
}
});
});
describe("FreeText (bugs)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must serialize invisible annotations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
let currentId = 0;
const expected = [];
const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1);
for (const pageNumber of oneToFourteen) {
const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
await scrollIntoView(page, pageSelector);
const annotationLayerSelector = `${pageSelector} > .annotationEditorLayer.freetextEditing`;
await page.waitForSelector(annotationLayerSelector, {
visible: true,
timeout: 0,
});
if (![1, 14].includes(pageNumber)) {
continue;
}
const rect = await getRect(page, annotationLayerSelector);
const editorSelector = getEditorSelector(currentId);
const data = `Hello PDF.js World !! on page ${pageNumber}`;
expected.push(data);
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await waitForSelectedEditor(page, editorSelector);
await waitForStorageEntries(page, currentId + 1);
const content = await page.$eval(editorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
currentId += 1;
}
const serialize = proprName =>
page.evaluate(name => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage
.serializable;
return map ? Array.from(map.values(), x => x[name]) : [];
}, proprName);
expect(await serialize("value"))
.withContext(`In ${browserName}`)
.toEqual(expected);
expect(await serialize("fontSize"))
.withContext(`In ${browserName}`)
.toEqual([10, 10]);
expect(await serialize("color"))
.withContext(`In ${browserName}`)
.toEqual([
[0, 0, 0],
[0, 0, 0],
]);
// Increase the font size for all the annotations.
await selectAll(page);
const [prevFontSize, prevColor] = await page.$eval(
".selectedEditor .internal",
el => {
const style = getComputedStyle(el);
return [style.fontSize, style.color];
}
);
page.evaluate(() => {
window.PDFViewerApplication.eventBus.dispatch(
"switchannotationeditorparams",
{
source: null,
type: window.pdfjsLib.AnnotationEditorParamsType.FREETEXT_SIZE,
value: 13,
}
);
});
await page.waitForFunction(
prev =>
getComputedStyle(
document.querySelector(".selectedEditor .internal")
).fontSize !== prev,
{},
prevFontSize
);
expect(await serialize("fontSize"))
.withContext(`In ${browserName}`)
.toEqual([13, 13]);
// Change the colors for all the annotations.
await page.evaluate(() => {
window.PDFViewerApplication.eventBus.dispatch(
"switchannotationeditorparams",
{
source: null,
type: window.pdfjsLib.AnnotationEditorParamsType.FREETEXT_COLOR,
value: "#FF0000",
}
);
});
await page.waitForFunction(
prev =>
getComputedStyle(
document.querySelector(".selectedEditor .internal")
).color !== prev,
{},
prevColor
);
expect(await serialize("color"))
.withContext(`In ${browserName}`)
.toEqual([
[255, 0, 0],
[255, 0, 0],
]);
})
);
});
});
describe("issue 15789", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("issue15789.pdf", ".annotationEditorLayer");
pages = await Promise.all(
pages.map(async ([browserName, page]) => {
await page.select("#scaleSelect", "1");
return [browserName, page];
})
);
});
afterAll(async () => {
await closePages(pages);
});
it("must take the media box into account", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
let currentId = 0;
for (let step = 0; step < 3; step++) {
await firstPageOnTop(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(currentId);
const data = `Hello ${step}`;
const x = Math.max(rect.x + 0.1 * rect.width, 10);
const y = Math.max(rect.y + 0.1 * rect.height, 10);
await page.mouse.click(x, y);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
const promise = await waitForAnnotationEditorLayer(page);
await page.evaluate(() => {
document.getElementById("pageRotateCw").click();
});
await awaitPromise(promise);
currentId += 1;
await page.waitForSelector(
".page[data-page-number='1'] .canvasWrapper",
{
timeout: 0,
}
);
await page.waitForSelector(
".page[data-page-number='1'] .annotationEditorLayer:not([hidden])",
{
timeout: 0,
}
);
}
const serialize = proprName =>
page.evaluate(name => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage
.serializable;
return map ? Array.from(map.values(), x => x[name]) : [];
}, proprName);
const rects = (await serialize("rect")).map(rect =>
rect.slice(0, 2).map(x => Math.floor(x))
);
const expected = [
[-28, 695],
[-38, -10],
[501, -20],
];
// Dimensions aren't exactly the same from a platform to an other
// so we're a bit tolerant here with the numbers.
// Anyway the goal is to check that the bottom left corner of the
// media box is taken into account.
// The pdf has a media box equals to [-99 -99 612.0 792.0].
const diffs = rects.map(
(rect, i) =>
Math.abs(rect[0] - expected[i][0]) < 10 &&
Math.abs(rect[1] - expected[i][1]) < 10
);
expect(diffs)
.withContext(`In ${browserName}`)
.toEqual([true, true, true]);
})
);
});
});
describe("FreeText (move existing)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("freetexts.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must move an annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
// All the current annotations should be serialized as null objects
// because they haven't been edited yet.
const serialized = await getSerialized(page);
expect(serialized).withContext(`In ${browserName}`).toEqual([]);
// Select the annotation we want to move.
const editorSelector = getEditorSelector(0);
await selectEditor(page, editorSelector);
await dragAndDrop(page, editorSelector, [[100, 100]]);
await waitForSerialized(page, 1);
})
);
});
});
describe("FreeText (update existing)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("freetexts.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must update an existing annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
// The page has been re-rendered but with no freetext annotations.
let isWhite = await isCanvasMonochrome(page, 1, null, 0xffffffff);
expect(isWhite).withContext(`In ${browserName}`).toBeTrue();
let editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
const editorSelector = getEditorSelector(0);
const editorRect = await getRect(page, editorSelector);
await selectEditor(page, editorSelector, /* count = */ 2);
await kbGoToEnd(page);
await page.waitForFunction(
sel =>
document.getSelection().anchorOffset ===
document.querySelector(sel).innerText.length,
{},
`${editorSelector} .internal`
);
await page.type(
`${editorSelector} .internal`,
" and edited in Firefox"
);
await commit(page);
const serialized = await getSerialized(page);
expect(serialized.length).withContext(`In ${browserName}`).toEqual(1);
expect(serialized[0]).toEqual(
jasmine.objectContaining({
color: [107, 217, 41],
fontSize: 14,
value: "Hello World from Acrobat and edited in Firefox",
id: "26R",
})
);
// Disable editing mode.
await switchToFreeText(page, /* disable = */ true);
// We want to check that the editor is displayed but not the original
// canvas.
editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1);
isWhite = await isCanvasMonochrome(page, 1, editorRect, 0xffffffff);
expect(isWhite).withContext(`In ${browserName}`).toBeTrue();
// Check we've now a div containing the text.
const newDivText = await page.$eval(
"[data-annotation-id='26R'] div.annotationContent",
el => el.innerText.replaceAll("\xa0", " ")
);
expect(newDivText)
.withContext(`In ${browserName}`)
.toEqual("Hello World from Acrobat and edited in Firefox");
// Re-enable editing mode.
await switchToFreeText(page);
await page.focus(".annotationEditorLayer");
await kbUndo(page);
await waitForSerialized(page, 0);
editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
// Undo again.
await kbUndo(page);
// Nothing should happen, it's why we can't wait for something
// specific!
// eslint-disable-next-line no-restricted-syntax
await waitForTimeout(200);
// We check that the editor hasn't been removed.
editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
})
);
});
});
describe("FreeText (update existing and popups)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("freetexts.pdf", "[data-annotation-id='32R']");
});
afterAll(async () => {
await closePages(pages);
});
it("must update an existing annotation and show the right popup", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Show the popup on "Hello World from Firefox"
await page.click("[data-annotation-id='32R']");
const popupSelector = "[data-annotation-id='popup_32R']";
await page.waitForSelector(popupSelector, { visible: true });
await switchToFreeText(page);
await page.waitForSelector(popupSelector, { visible: false });
const editorSelector = getEditorSelector(1);
await selectEditor(page, editorSelector, /* count = */ 2);
await kbGoToEnd(page);
await page.waitForFunction(
sel =>
document.getSelection().anchorOffset ===
document.querySelector(sel).innerText.length,
{},
`${editorSelector} .internal`
);
await page.type(
`${editorSelector} .internal`,
" and edited in Firefox"
);
await commit(page);
// Disable editing mode.
await switchToFreeText(page, /* disable = */ true);
await page.waitForSelector(popupSelector, { visible: true });
const newPopupText = await page.$eval(
`${popupSelector} .popupContent`,
el => el.innerText.replaceAll("\xa0", " ")
);
expect(newPopupText)
.withContext(`In ${browserName}`)
.toEqual("Hello World From Firefox and edited in Firefox");
})
);
});
});
describe("FreeText (update existing but not empty ones)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("issue14438.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must update an existing annotation but not an empty one", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1);
})
);
});
});
describe("FreeText (delete existing)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("freetexts.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must delete an existing annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
let editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
const editorSelector = getEditorSelector(3);
await selectEditor(page, editorSelector);
await page.keyboard.press("Backspace");
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
editorSelector
);
const serialized = await getSerialized(page);
expect(serialized).toEqual([
{
pageIndex: 0,
id: "51R",
deleted: true,
popupRef: "",
},
]);
// Disable editing mode.
await switchToFreeText(page, /* disable = */ true);
// We want to check that nothing is displayed.
editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(0);
const hidden = await page.$eval(
"[data-annotation-id='51R']",
el => el.hidden
);
expect(hidden).withContext(`In ${browserName}`).toBeTrue();
// Re-enable editing mode.
await switchToFreeText(page);
await page.focus(".annotationEditorLayer");
await kbUndo(page);
await waitForSerialized(page, 0);
})
);
});
});
describe("FreeText (copy/paste existing)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("freetexts.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must copy and paste an existing annotation", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToFreeText(page);
const editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
await selectEditor(page, getEditorSelector(1));
await copy(page);
await paste(page);
await page.waitForSelector(getEditorSelector(6), { visible: true });
await waitForStorageEntries(page, 7);
}
});
});
describe("FreeText (edit existing in double clicking on it)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("freetexts.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must edit an annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const modeChangedHandle = await waitForAnnotationModeChanged(page);
await page.click("[data-annotation-id='26R']", { count: 2 });
await awaitPromise(modeChangedHandle);
await page.waitForSelector(`${getEditorSelector(0)}-editor`);
const [focusedId, editable] = await page.evaluate(() => {
const el = document.activeElement;
return [el.id, el.contentEditable];
});
expect(focusedId)
.withContext(`In ${browserName}`)
.toEqual("pdfjs_internal_editor_0-editor");
expect(editable).withContext(`In ${browserName}`).toEqual("true");
const editorIds = await getEditors(page, "freeText");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
})
);
});
});
describe("FreeText with popup", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"annotation-freetext.pdf",
".annotationEditorLayer"
);
});
afterAll(async () => {
await closePages(pages);
});
it("must not remove an empty annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector("[data-annotation-id='23R']");
// Cannot use page.hover with Firefox on Mac because of a bug.
// TODO: remove this when we switch to BiDi.
await hover(page, "[data-annotation-id='23R']");
// Wait for the popup to be displayed.
await page.waitForFunction(
() =>
document.querySelector("[data-annotation-id='popup_23R']")
.hidden === false
);
// Enter in editing mode.
await switchToFreeText(page);
// Disable editing mode.
await switchToFreeText(page, /* disable = */ true);
// TODO: remove this when we switch to BiDi.
await hover(page, "[data-annotation-id='23R']");
// Wait for the popup to be displayed.
await page.waitForFunction(
() =>
document.querySelector("[data-annotation-id='popup_23R']")
.hidden === false
);
})
);
});
it("must hide the popup when editing", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("[data-annotation-id='20R']");
// Wait for the popup to be displayed.
await page.waitForFunction(
() =>
document.querySelector("[data-annotation-id='popup_20R']")
.hidden === false
);
// Enter in editing mode.
await switchToFreeText(page);
// Wait for the popup to be hidden.
await page.waitForFunction(
() =>
document.querySelector("[data-annotation-id='popup_20R']")
.hidden === true
);
// Exit editing mode.
await switchToFreeText(page, /* disable = */ true);
// Wait for the popup to be visible.
await page.waitForFunction(
() =>
document.querySelector("[data-annotation-id='popup_20R']")
.hidden === false
);
})
);
});
});
describe("FreeText rotation", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that the dimensions of a rotated annotations are correct after a font size change", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.evaluate(() => {
window.PDFViewerApplication.rotatePages(90);
});
await page.waitForSelector(
".annotationEditorLayer[data-main-rotation='90']"
);
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
// Make Chrome happy.
await page.waitForFunction(() => {
const box = [
...window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map.values(),
][0].rect;
return box[2] !== box[0];
}, {});
let serialized = await getSerialized(page);
let bbox = serialized[0].rect;
let width = bbox[2] - bbox[0];
let height = bbox[3] - bbox[1];
expect(width < height)
.withContext(`In ${browserName}`)
.toEqual(true);
await page.evaluate(() => {
window.PDFViewerApplication.rotatePages(270);
});
await page.waitForSelector(
".annotationEditorLayer[data-main-rotation='0']"
);
await selectAll(page);
const { width: prevWidth } = await getRect(
page,
".selectedEditor .internal"
);
page.evaluate(() => {
window.PDFViewerApplication.eventBus.dispatch(
"switchannotationeditorparams",
{
source: null,
type: window.pdfjsLib.AnnotationEditorParamsType.FREETEXT_SIZE,
value: 50,
}
);
});
await page.waitForFunction(
prev =>
document
.querySelector(".selectedEditor .internal")
.getBoundingClientRect().width !== prev,
{},
prevWidth
);
// Make Chrome happy.
await page.waitForFunction(() => {
const box = [
...window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map.values(),
][0].rect;
return box[2] !== box[0];
}, {});
serialized = await getSerialized(page);
bbox = serialized[0].rect;
width = bbox[2] - bbox[0];
height = bbox[3] - bbox[1];
expect(width < height)
.withContext(`In ${browserName}`)
.toEqual(true);
})
);
});
});
describe("FreeText (remove)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must delete invisible annotations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
let currentId = 0;
const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1);
for (const pageNumber of oneToFourteen) {
const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
await scrollIntoView(page, pageSelector);
const annotationLayerSelector = `${pageSelector} > .annotationEditorLayer.freetextEditing`;
await page.waitForSelector(annotationLayerSelector, {
visible: true,
timeout: 0,
});
if (![1, 14].includes(pageNumber)) {
continue;
}
const rect = await getRect(page, annotationLayerSelector);
const editorSelector = getEditorSelector(currentId);
const data = `Hello PDF.js World !! on page ${pageNumber}`;
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
currentId += 1;
}
await selectAll(page);
const serialize = () =>
page.evaluate(() => {
const { map } =
window.PDFViewerApplication.pdfDocument.annotationStorage
.serializable;
return map ? Array.from(map.values(), x => x.pageIndex) : [];
});
expect(await serialize())
.withContext(`In ${browserName}`)
.toEqual([0, 13]);
// Delete
await page.keyboard.press("Backspace");
await waitForSerialized(page, 0);
})
);
});
});
describe("FreeText (open existing)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("issue16633.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must open an existing annotation and check that the position are good", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const toBinary = buf => {
for (let i = 0; i < buf.length; i += 4) {
const gray =
(0.2126 * buf[i] + 0.7152 * buf[i + 1] + 0.0722 * buf[i + 2]) /
255;
buf[i] = buf[i + 1] = buf[i + 2] = gray <= 0.5 ? 0 : 255;
}
};
// We want to detect the first non-white pixel in the image.
// But we can have some antialiasing...
// The idea to just try to detect the beginning of the vertical bar
// of the "H" letter.
// Hence we just take the first non-white pixel in the image which is
// the most repeated one.
const getFirstPixel = (buf, width, height) => {
toBinary(buf);
const firsts = [];
const stats = {};
// Get the position of the first pixels.
// The position of char depends on a lot of different parameters,
// hence it's possible to not have a pixel where we expect to have
// it. So we just collect the positions of the first black pixel and
// take the first one where its abscissa is the most frequent.
for (let i = height - 1; i >= 0; i--) {
for (let j = 0; j < width; j++) {
const idx = (width * i + j) << 2;
if (buf[idx] === 0) {
firsts.push([j, i]);
stats[j] = (stats[j] || 0) + 1;
break;
}
}
}
let maxValue = -Infinity;
let maxJ = 0;
for (const [j, count] of Object.entries(stats)) {
if (count > maxValue) {
maxValue = count;
maxJ = j;
}
}
maxJ = parseInt(maxJ, 10);
for (const [j, i] of firsts) {
if (j === maxJ) {
return [j, i];
}
}
return null;
};
const firstPixelsAnnotations = new Map();
// [26, 32, ...] are the annotation ids
for (const n of [26, 32, 42, 57, 35, 1]) {
const id = `${n}R`;
const rect = await getRect(page, `[data-annotation-id="${id}"]`);
const editorPng = await page.screenshot({
clip: rect,
type: "png",
});
const editorImage = PNG.sync.read(Buffer.from(editorPng));
const editorFirstPix = getFirstPixel(
editorImage.data,
editorImage.width,
editorImage.height
);
firstPixelsAnnotations.set(id, { editorFirstPix, rect });
}
await switchToFreeText(page);
await page.evaluate(() => {
document.getElementById("editorFreeTextParamsToolbar").remove();
});
for (const n of [0, 1, 2, 3, 4]) {
const annotationId = await page.evaluate(N => {
const editor = document.getElementById(
`pdfjs_internal_editor_${N}`
);
return editor.getAttribute("annotation-id");
}, n);
const { editorFirstPix: annotationFirstPix, rect } =
firstPixelsAnnotations.get(annotationId);
const editorPng = await page.screenshot({
clip: rect,
type: "png",
});
const editorImage = PNG.sync.read(Buffer.from(editorPng));
const editorFirstPix = getFirstPixel(
editorImage.data,
editorImage.width,
editorImage.height
);
expect(
Math.abs(editorFirstPix[0] - annotationFirstPix[0]) <= 3 &&
Math.abs(editorFirstPix[1] - annotationFirstPix[1]) <= 3
)
.withContext(
`In ${browserName}, first pix coords in editor: ${editorFirstPix} and in annotation: ${annotationFirstPix}`
)
.toEqual(true);
}
})
);
});
});
describe("FreeText (open existing and rotated)", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"rotated_freetexts.pdf",
".annotationEditorLayer",
100
);
});
afterAll(async () => {
await closePages(pages);
});
it("must open an existing rotated annotation and check that the position are good", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const toBinary = buf => {
for (let i = 0; i < buf.length; i += 4) {
const gray =
(0.2126 * buf[i] + 0.7152 * buf[i + 1] + 0.0722 * buf[i + 2]) /
255;
buf[i] = buf[i + 1] = buf[i + 2] = gray >= 0.5 ? 255 : 0;
}
};
const getFirstPixel = (buf, width, height, start) => {
toBinary(buf);
const firsts = [];
const stats = {};
switch (start) {
case "TL":
for (let j = 0; j < width; j++) {
for (let i = 0; i < height; i++) {
const idx = (width * i + j) << 2;
if (buf[idx] === 0) {
firsts.push([j, i]);
stats[j] = (stats[j] || 0) + 1;
break;
}
}
}
break;
case "TR":
for (let i = 0; i < height; i++) {
for (let j = width - 1; j >= 0; j--) {
const idx = (width * i + j) << 2;
if (buf[idx] === 0) {
firsts.push([j, i]);
stats[j] = (stats[j] || 0) + 1;
break;
}
}
}
break;
case "BR":
for (let j = width - 1; j >= 0; j--) {
for (let i = height - 1; i >= 0; i--) {
const idx = (width * i + j) << 2;
if (buf[idx] === 0) {
firsts.push([j, i]);
stats[j] = (stats[j] || 0) + 1;
break;
}
}
}
break;
case "BL":
for (let i = height - 1; i >= 0; i--) {
for (let j = 0; j < width; j++) {
const idx = (width * i + j) << 2;
if (buf[idx] === 0) {
firsts.push([j, i]);
stats[j] = (stats[j] || 0) + 1;
break;
}
}
}
break;
}
let maxValue = -Infinity;
let maxJ = 0;
for (const [j, count] of Object.entries(stats)) {
if (count > maxValue) {
maxValue = count;
maxJ = j;
}
}
maxJ = parseInt(maxJ, 10);
for (const [j, i] of firsts) {
if (j === maxJ) {
return [j, i];
}
}
return null;
};
const firstPixelsAnnotations = new Map();
for (const [n, start] of [
[17, "BL"],
[18, "BR"],
[19, "TR"],
[20, "TL"],
]) {
const id = `${n}R`;
const rect = await getRect(page, `[data-annotation-id="${id}"]`);
const editorPng = await page.screenshot({
clip: rect,
type: "png",
});
const editorImage = PNG.sync.read(Buffer.from(editorPng));
const editorFirstPix = getFirstPixel(
editorImage.data,
editorImage.width,
editorImage.height,
start
);
firstPixelsAnnotations.set(id, { editorFirstPix, rect });
}
await switchToFreeText(page);
await page.evaluate(() => {
document.getElementById("editorFreeTextParamsToolbar").remove();
});
for (const [n, start] of [
[0, "BL"],
[1, "BR"],
[2, "TR"],
[3, "TL"],
]) {
const annotationId = await page.evaluate(N => {
const editor = document.getElementById(
`pdfjs_internal_editor_${N}`
);
return editor.getAttribute("annotation-id");
}, n);
const { editorFirstPix: annotationFirstPix, rect } =
firstPixelsAnnotations.get(annotationId);
const editorPng = await page.screenshot({
clip: rect,
type: "png",
});
const editorImage = PNG.sync.read(Buffer.from(editorPng));
const editorFirstPix = getFirstPixel(
editorImage.data,
editorImage.width,
editorImage.height,
start
);
expect(
Math.abs(editorFirstPix[0] - annotationFirstPix[0]) <= 3 &&
Math.abs(editorFirstPix[1] - annotationFirstPix[1]) <= 3
)
.withContext(
`In ${browserName}, first pix coords in editor: ${editorFirstPix} and in annotation: ${annotationFirstPix}`
)
.toEqual(true);
}
})
);
});
});
describe("Keyboard shortcuts when the editor layer isn't focused", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that the shortcuts are working correctly", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await page.focus("#editorFreeTextColor");
await kbUndo(page);
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
editorSelector
);
await kbRedo(page);
await page.waitForFunction(
sel => !!document.querySelector(sel),
{},
editorSelector
);
})
);
});
});
describe("Move editor with arrows", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check the position of moved editor", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
const editorSelector = getEditorSelector(0);
await page.mouse.click(rect.x + 200, rect.y + 200);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
const [pageX, pageY] = await getFirstSerialized(page, x => x.rect);
await moveEditor(page, editorSelector, 20, () =>
page.keyboard.press("ArrowRight")
);
let [newX, newY] = await getFirstSerialized(page, x => x.rect);
expect(Math.round(newX))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageX + 20));
expect(Math.round(newY))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageY));
await moveEditor(page, editorSelector, 20, () =>
page.keyboard.press("ArrowDown")
);
[newX, newY] = await getFirstSerialized(page, x => x.rect);
expect(Math.round(newX))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageX + 20));
expect(Math.round(newY))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageY - 20));
await moveEditor(page, editorSelector, 2, () => kbBigMoveLeft(page));
[newX, newY] = await getFirstSerialized(page, x => x.rect);
expect(Math.round(newX))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageX));
expect(Math.round(newY))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageY - 20));
await moveEditor(page, editorSelector, 2, () => kbBigMoveUp(page));
[newX, newY] = await getFirstSerialized(page, x => x.rect);
expect(Math.round(newX))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageX));
expect(Math.round(newY))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageY));
})
);
});
it("must check arrow doesn't move an editor when a slider is focused", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
const editorSelector = getEditorSelector(0);
await page.mouse.click(rect.x + 200, rect.y + 200);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await selectAll(page);
await page.focus("#editorFreeTextFontSize");
const [page1X, , page2X] = await getFirstSerialized(
page,
x => x.rect
);
const pageWidth = page2X - page1X;
await moveEditor(page, editorSelector, 5, () =>
page.keyboard.press("ArrowRight")
);
const [new1X, , new2X] = await getFirstSerialized(page, x => x.rect);
const newWidth = new2X - new1X;
expect(Math.round(new1X))
.withContext(`In ${browserName}`)
.not.toEqual(Math.round(page1X + 5));
expect(newWidth)
.withContext(`In ${browserName}`)
.not.toEqual(pageWidth);
})
);
});
it("must check the position of an empty freetext", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
let editorSelector = getEditorSelector(0);
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
const [pageX, pageY] = await getFirstSerialized(page, x => x.rect);
await clearAll(page);
editorSelector = getEditorSelector(1);
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await moveEditor(page, editorSelector, 20, () =>
page.keyboard.press("ArrowRight")
);
await moveEditor(page, editorSelector, 2, () => kbBigMoveDown(page));
await moveEditor(page, editorSelector, 20, () =>
page.keyboard.press("ArrowLeft")
);
await moveEditor(page, editorSelector, 2, () => kbBigMoveUp(page));
await moveEditor(page, editorSelector, 2, () => kbBigMoveRight(page));
await moveEditor(page, editorSelector, 2, () => kbBigMoveLeft(page));
await page.type(`${editorSelector} .internal`, data);
await commit(page);
const [newX, newY] = await getFirstSerialized(page, x => x.rect);
expect(Math.round(newX))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageX));
expect(Math.round(newY))
.withContext(`In ${browserName}`)
.toEqual(Math.round(pageY));
})
);
});
});
describe("Focus must go on the current page", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
100,
{
eventBusSetup: eventBus => {
window.visitedPages = [];
eventBus.on("pagechanging", ({ pageNumber }) => {
window.visitedPages.push(pageNumber);
});
},
}
);
});
afterAll(async () => {
await closePages(pages);
});
it("must check that the focus is on the right page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await cancelFocusIn(page, editorSelector);
await commit(page);
const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1);
for (const pageNumber of oneToFourteen) {
const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
await scrollIntoView(page, pageSelector);
const annotationLayerSelector = `${pageSelector} > .annotationEditorLayer:not([hidden]).freetextEditing`;
await page.waitForSelector(annotationLayerSelector, {
visible: true,
timeout: 0,
});
}
const visitedPages = await page.evaluate(() => {
const p = window.visitedPages;
delete window.visitedPages;
return p;
});
const sorted = visitedPages.slice().sort((a, b) => a - b);
expect(visitedPages.length)
.withContext(`In ${browserName}`)
.not.toEqual(0);
expect(visitedPages).withContext(`In ${browserName}`).toEqual(sorted);
})
);
});
});
describe("Freetext must stay focused after having been moved", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must keep the focus", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
let rect = await getRect(page, ".annotationEditorLayer");
const firstEditorSelector = getEditorSelector(0);
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(firstEditorSelector, { visible: true });
await page.type(`${firstEditorSelector} .internal`, "A");
await commit(page);
2024-04-17 21:10:23 +02:00
// Create a new editor.
rect = await getRect(page, firstEditorSelector);
const secondEditorSelector = getEditorSelector(1);
await page.mouse.click(
rect.x + 5 * rect.width,
rect.y + 5 * rect.height
);
await page.waitForSelector(secondEditorSelector, { visible: true });
await page.type(`${secondEditorSelector} .internal`, "B");
await commit(page);
2024-04-17 21:10:23 +02:00
// Select the second editor.
await selectEditor(page, secondEditorSelector);
const pos = selector =>
page.evaluate(sel => {
const editor = document.querySelector(sel);
return Array.prototype.indexOf.call(
editor.parentNode.childNodes,
editor
);
}, selector);
expect(await pos(firstEditorSelector))
.withContext(`In ${browserName}`)
.toEqual(0);
expect(await pos(secondEditorSelector))
.withContext(`In ${browserName}`)
.toEqual(1);
const { y: y0, height } = await getRect(page, firstEditorSelector);
const editorSelector = secondEditorSelector;
while ((await getRect(page, editorSelector)).y > y0 - height) {
await moveEditor(page, editorSelector, 1, () => kbBigMoveUp(page));
}
// The editor must be moved in the DOM and potentially the focus
// will be lost, hence there's a callback will get back the focus.
await page.waitForSelector(`${secondEditorSelector}:focus`);
expect(await pos(firstEditorSelector))
.withContext(`In ${browserName}`)
.toEqual(1);
expect(await pos(secondEditorSelector))
.withContext(`In ${browserName}`)
.toEqual(0);
})
);
});
});
describe("Move several FreeTexts", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must move several annotations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const allPositions = [];
for (let i = 0; i < 10; i++) {
const editorSelector = getEditorSelector(i);
await page.mouse.click(rect.x + 10 + 30 * i, rect.y + 100 + 5 * i);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(
`${editorSelector} .internal`,
String.fromCharCode(65 + i)
);
await commit(page);
allPositions.push(await getRect(page, editorSelector));
}
await selectAll(page);
await dragAndDrop(page, getEditorSelector(4), [[39, 74]]);
for (let i = 0; i < 10; i++) {
const editorSelector = getEditorSelector(i);
const pos = await getRect(page, editorSelector);
const oldPos = allPositions[i];
expect(Math.abs(Math.round(pos.x - oldPos.x) - 39))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
expect(Math.abs(Math.round(pos.y - oldPos.y) - 74))
.withContext(`In ${browserName}`)
.toBeLessThanOrEqual(1);
}
})
);
});
});
describe("Don't unselect all when scrolling", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".annotationEditorLayer",
100,
{
eventBusSetup: eventBus => {
eventBus.on("annotationeditorstateschanged", ({ details }) => {
window.editingEvents?.push(details);
});
},
}
);
});
afterAll(async () => {
await closePages(pages);
});
it("must check that selected editor stay selected", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await cancelFocusIn(page, editorSelector);
await commit(page);
await page.evaluate(() => {
window.editingEvents = [];
});
for (let pageNumber = 1; pageNumber <= 4; pageNumber++) {
const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
await scrollIntoView(page, pageSelector);
const annotationLayerSelector = `${pageSelector} > .annotationEditorLayer.freetextEditing`;
await page.waitForSelector(annotationLayerSelector, {
visible: true,
timeout: 0,
});
}
const editingEvents = await page.evaluate(() => {
const e = window.editingEvents;
delete window.editingEvents;
return e;
});
expect(editingEvents.length)
.withContext(`In ${browserName}`)
.toEqual(0);
})
);
});
});
describe("FreeText on several pages", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that first annotation is selected without errors", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const page1Selector = `.page[data-page-number = "1"] > .annotationEditorLayer.freetextEditing`;
let rect = await getRect(page, page1Selector);
const firstEditorSelector = getEditorSelector(0);
await page.mouse.click(rect.x + 10, rect.y + 10);
await page.waitForSelector(firstEditorSelector, { visible: true });
await page.type(`${firstEditorSelector} .internal`, "Hello");
await cancelFocusIn(page, firstEditorSelector);
await commit(page);
2023-10-26 22:44:35 +02:00
// Unselect.
await unselectEditor(page, firstEditorSelector);
2023-10-26 22:44:35 +02:00
// Select the editor created previously.
await selectEditor(page, firstEditorSelector);
// Go to the last page.
2023-10-26 22:44:35 +02:00
await scrollIntoView(page, `.page[data-page-number = "14"]`);
const page14Selector = `.page[data-page-number = "14"] > .annotationEditorLayer.freetextEditing`;
await page.waitForSelector(page14Selector, {
visible: true,
timeout: 0,
});
rect = await getRect(page, page14Selector);
const secondEditorSelector = getEditorSelector(1);
await page.mouse.click(rect.x + 10, rect.y + 10);
await page.waitForSelector(secondEditorSelector, { visible: true });
await page.type(`${secondEditorSelector} .internal`, "World");
await commit(page);
for (let i = 0; i < 13; i++) {
await page.keyboard.press("P");
const pageSelector = `.page[data-page-number = "${
13 - i
}"] > .annotationEditorLayer.freetextEditing`;
await page.waitForSelector(pageSelector, {
visible: true,
timeout: 0,
});
}
await page.waitForSelector(firstEditorSelector, { visible: true });
await selectEditor(page, firstEditorSelector);
const content = await page.$eval(firstEditorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual("Hello");
})
);
});
});
describe("Deleted FreeText", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that a deleted freetext can be restored", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const page1Selector = `.page[data-page-number = "1"] > .annotationEditorLayer.freetextEditing`;
const rect = await getRect(page, page1Selector);
const editorSelector = getEditorSelector(0);
await page.mouse.click(rect.x + 10, rect.y + 10);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, "Hello");
await cancelFocusIn(page, editorSelector);
await commit(page);
2023-10-26 22:44:35 +02:00
// Unselect.
await unselectEditor(page, editorSelector);
2023-10-26 22:44:35 +02:00
// Select the editor created previously.
await selectEditor(page, editorSelector);
// Go to the last page.
2023-10-26 22:44:35 +02:00
await scrollIntoView(page, `.page[data-page-number = "14"]`);
await page.waitForSelector(
`.page[data-page-number = "14"] > .annotationEditorLayer.freetextEditing`,
{ visible: true, timeout: 0 }
);
await clearAll(page);
// Go to the first page.
await kbGoToBegin(page);
await page.waitForSelector(page1Selector, {
visible: true,
timeout: 0,
});
// Make sure that nothing has be added.
await waitForStorageEntries(page, 0);
await kbUndo(page);
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector, { visible: true });
})
);
});
});
describe("FreeText accessibility", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("bug1823296.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that the parent structTree id is correct", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const parentId = "p3R_mc8";
const editorSelector = getEditorSelector(0);
const rect = await page.evaluate(id => {
const parent = document.getElementById(id);
let span = null;
for (const child of parent.childNodes) {
if (child.innerText === "000.[5]") {
span = child;
break;
}
}
const { x, y, width, height } = span.getBoundingClientRect();
return { x, y, width, height };
}, parentId);
await page.mouse.click(
rect.x + rect.width + 5,
rect.y + rect.height / 2
);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, "Hello Wolrd");
await commit(page);
await waitForStorageEntries(page, 1);
const id = await getFirstSerialized(page, x => x.structTreeParentId);
expect(id).withContext(`In ${browserName}`).toEqual(parentId);
})
);
});
});
describe("Bug 1854818: mouse events in a selected FreeText editor", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check the text can be selected with the mouse", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
const internalEditorSelector = `${editorSelector} .internal`;
await page.type(internalEditorSelector, data);
await commit(page);
await page.click(editorSelector, { count: 2 });
await page.waitForSelector(
`${editorSelector} .overlay:not(.enabled)`
);
await page.click(internalEditorSelector, {
count: 3,
});
const selection = await page.evaluate(() =>
document.getSelection().toString()
);
expect(selection).withContext(`In ${browserName}`).toEqual(data);
})
);
});
});
describe("Create editor with keyboard", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must create an editor, using Enter/Space keys, with focused toolbar button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.focus("#editorFreeTextButton");
await page.keyboard.press("Enter");
let editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await moveEditor(page, editorSelector, 5, () => kbBigMoveUp(page));
const data = "Hello PDF.js World !!";
await page.type(`${editorSelector} .internal`, data);
await commit(page);
let content = await page.$eval(editorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
// Disable editing mode.
await switchToFreeText(page, /* disable = */ true);
await page.focus("#editorFreeTextButton");
await page.keyboard.press(" ");
editorSelector = getEditorSelector(1);
await page.waitForSelector(editorSelector, { visible: true });
await moveEditor(page, editorSelector, 5, () => kbBigMoveDown(page));
await page.type(`${editorSelector} .internal`, data);
await commit(page);
// Unselect.
await unselectEditor(page, editorSelector);
content = await page.$eval(editorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
})
);
});
it("must create an editor, using Enter/Space keys, with focused page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
await page.focus(".annotationEditorLayer");
await page.keyboard.press("Enter");
let editorSelector = getEditorSelector(0);
await page.waitForSelector(editorSelector, { visible: true });
await moveEditor(page, editorSelector, 10, () => kbBigMoveLeft(page));
const data = "Hello PDF.js World !!";
await page.type(`${editorSelector} .internal`, data);
await commit(page);
// Unselect.
await unselectEditor(page, editorSelector);
let content = await page.$eval(editorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
await page.keyboard.press(" ");
editorSelector = getEditorSelector(1);
await page.waitForSelector(editorSelector, { visible: true });
await moveEditor(page, editorSelector, 10, () =>
kbBigMoveRight(page)
);
await page.type(`${editorSelector} .internal`, data);
await commit(page);
// Unselect.
await unselectEditor(page, editorSelector);
content = await page.$eval(editorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
})
);
});
});
describe("Avoid to steal keyboard events", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check the keyboard event is limited to the input", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
let handle = await createPromise(page, resolve => {
document.addEventListener("selectionchange", resolve, {
once: true,
});
});
await page.click("#pageNumber");
await awaitPromise(handle);
handle = await createPromise(page, resolve => {
document
.getElementById("pageNumber")
.addEventListener("keyup", resolve, { once: true });
});
await page.keyboard.press("Backspace");
await awaitPromise(handle);
let content = await page.$eval("#pageNumber", el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual("");
content = await page.$eval(editorSelector, el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
})
);
});
});
describe("Delete a freetext in using the delete button", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that a freetext is deleted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
// Delete it in using the button.
await page.click(`${editorSelector} button.delete`);
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
editorSelector
);
await waitForStorageEntries(page, 0);
// Undo.
await kbUndo(page);
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector, { visible: true });
})
);
});
});
describe("Delete two freetexts in using the delete button and the keyboard", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that freetexts are deleted", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
for (let i = 1; i <= 2; i++) {
const editorSelector = getEditorSelector(i - 1);
await page.mouse.click(rect.x + i * 100, rect.y + i * 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
}
// Select the editor created previously.
const editorSelector = getEditorSelector(0);
await selectEditor(page, editorSelector);
await selectAll(page);
// Delete it in using the button.
await page.focus(`${editorSelector} button.delete`);
await page.keyboard.press("Enter");
await page.waitForFunction(
sel => !document.querySelector(sel),
{},
editorSelector
);
await waitForStorageEntries(page, 0);
// Undo.
await kbUndo(page);
await waitForSerialized(page, 2);
await page.waitForSelector(editorSelector, { visible: true });
await page.waitForSelector(getEditorSelector(1), { visible: true });
})
);
});
});
describe("Consecutive white spaces in Freetext without appearance", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("bug1871353.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that consecutive white spaces are preserved when a freetext is edited", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const editorSelector = getEditorSelector(0);
await page.click(editorSelector, { count: 2 });
await page.type(`${editorSelector} .internal`, "C");
await switchToFreeText(page, /* disable = */ true);
const [value] = await getSerialized(page, x => x.value);
expect(value)
.withContext(`In ${browserName}`)
.toEqual("CA B");
})
);
});
});
describe("Consecutive white spaces in Freetext with appearance", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("bug1871353.1.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that consecutive white spaces are preserved when a freetext is edited", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const editorSelector = getEditorSelector(0);
await page.click(editorSelector, { count: 2 });
await page.type(`${editorSelector} .internal`, "Z");
await switchToFreeText(page, /* disable = */ true);
const [value] = await getSerialized(page, x => x.value);
expect(value)
.withContext(`In ${browserName}`)
.toEqual("ZX Y");
})
);
});
});
describe("Freetext with several lines", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that all lines are correctly exported", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello\nPDF.js\nWorld\n!!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await waitForSerialized(page, 1);
const serialized = (await getSerialized(page))[0];
expect(serialized.value)
.withContext(`In ${browserName}`)
.toEqual(data);
})
);
});
});
describe("Freetext UI when undoing/redoing", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that the parameters are updated when undoing/redoing", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await page.evaluate(() => {
window.PDFViewerApplication.eventBus.dispatch(
"switchannotationeditorparams",
{
source: null,
type: window.pdfjsLib.AnnotationEditorParamsType.FREETEXT_COLOR,
value: "#FF0000",
}
);
});
await page.waitForFunction(
() =>
getComputedStyle(
document.querySelector(".selectedEditor .internal")
).color === "rgb(255, 0, 0)"
);
await kbUndo(page);
await page.waitForFunction(
() =>
getComputedStyle(
document.querySelector(".selectedEditor .internal")
).color === "rgb(0, 0, 0)"
);
await page.waitForFunction(
() =>
document.getElementById("editorFreeTextColor").value === "#000000"
);
await kbRedo(page);
await page.waitForFunction(
() =>
getComputedStyle(
document.querySelector(".selectedEditor .internal")
).color === "rgb(255, 0, 0)"
);
await page.waitForFunction(
() =>
document.getElementById("editorFreeTextColor").value === "#ff0000"
);
})
);
});
});
describe("Annotation editor layer visibility", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that the annotation layer is visible after a rotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
for (let i = 0; i < 4; i++) {
await page.waitForSelector(
".page[data-page-number='1'] .annotationEditorLayer:not([hidden])",
{
timeout: 0,
}
);
const promise = await waitForAnnotationEditorLayer(page);
await page.evaluate(() => {
document.getElementById("pageRotateCw").click();
});
await awaitPromise(promise);
}
})
);
});
});
describe("Delete a freetext and undo it on another page", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that a freetext can be undone", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
const twoToFourteen = Array.from(new Array(13).keys(), n => n + 2);
for (const pageNumber of twoToFourteen) {
const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
await scrollIntoView(page, pageSelector);
}
await kbUndo(page);
await waitForSerialized(page, 1);
const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n);
for (const pageNumber of thirteenToOne) {
const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
await scrollIntoView(page, pageSelector);
}
await page.waitForSelector(editorSelector);
})
);
});
});
describe("Delete a freetext, scroll and undo it", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that a freetext can be undone", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
const twoToOne = Array.from(new Array(13).keys(), n => n + 2).concat(
Array.from(new Array(13).keys(), n => 13 - n)
);
for (const pageNumber of twoToOne) {
const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
await scrollIntoView(page, pageSelector);
}
await kbUndo(page);
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
})
);
});
});
describe("Paste some html", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that pasting html just keep the text", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
let editorSelector = getEditorSelector(0);
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
await page.type(`${editorSelector} .internal`, data);
await commit(page);
const waitForTextChange = (previous, edSelector) =>
page.waitForFunction(
(prev, sel) => document.querySelector(sel).innerText !== prev,
{},
previous,
`${edSelector} .internal`
);
const getText = edSelector =>
page.$eval(`${edSelector} .internal`, el => el.innerText.trimEnd());
await selectEditor(page, editorSelector, /* count = */ 2);
const select = position =>
page.evaluate(
(sel, pos) => {
const el = document.querySelector(sel);
document.getSelection().setPosition(el.firstChild, pos);
},
`${editorSelector} .internal`,
position
);
await select(0);
await copyToClipboard(page, {
"text/html": "<b>Bold Foo</b>",
"text/plain": "Foo",
});
await pasteFromClipboard(page, `${editorSelector} .internal`);
let lastText = data;
await waitForTextChange(lastText, editorSelector);
let text = await getText(editorSelector);
lastText = `Foo${data}`;
expect(text).withContext(`In ${browserName}`).toEqual(lastText);
await select(3);
await copyToClipboard(page, {
"text/html": "<b>Bold Bar</b><br><b>Oof</b>",
"text/plain": "Bar\nOof",
});
await pasteFromClipboard(page, `${editorSelector} .internal`);
await waitForTextChange(lastText, editorSelector);
text = await getText(editorSelector);
lastText = `FooBar\nOof${data}`;
expect(text).withContext(`In ${browserName}`).toEqual(lastText);
await select(0);
await copyToClipboard(page, { "text/html": "<b>basic html</b>" });
await pasteFromClipboard(page, `${editorSelector} .internal`);
// Nothing should change, so it's hard to wait on something.
// eslint-disable-next-line no-restricted-syntax
await waitForTimeout(100);
text = await getText(editorSelector);
expect(text).withContext(`In ${browserName}`).toEqual(lastText);
const getHTML = () =>
page.$eval(`${editorSelector} .internal`, el => el.innerHTML);
const prevHTML = await getHTML();
// Try to paste an image.
await copyToClipboard(page, {
"image/png":
// 1x1 transparent png.
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
});
await pasteFromClipboard(page, `${editorSelector} .internal`);
// Nothing should change, so it's hard to wait on something.
// eslint-disable-next-line no-restricted-syntax
await waitForTimeout(100);
const html = await getHTML();
expect(html).withContext(`In ${browserName}`).toEqual(prevHTML);
await commit(page);
editorSelector = getEditorSelector(1);
await page.mouse.click(rect.x + 200, rect.y + 200);
await page.waitForSelector(editorSelector, { visible: true });
const fooBar = "Foo\nBar\nOof";
await copyToClipboard(page, {
"text/html": "<b>html</b>",
"text/plain": fooBar,
});
await pasteFromClipboard(page, `${editorSelector} .internal`);
await waitForTextChange("", editorSelector);
text = await getText(editorSelector);
expect(text).withContext(`In ${browserName}`).toEqual(fooBar);
}
});
});
describe("Update a freetext and scroll", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait(
"tracemonkey_freetext.pdf",
".annotationEditorLayer"
);
});
afterAll(async () => {
await closePages(pages);
});
it("must check that a freetext is still there after having updated it and scroll the doc", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const editorSelector = getEditorSelector(0);
const editorRect = await getRect(page, editorSelector);
await selectEditor(page, editorSelector, /* count = */ 2);
await kbGoToEnd(page);
await page.waitForFunction(
sel =>
document.getSelection().anchorOffset ===
document.querySelector(sel).innerText.length,
{},
`${editorSelector} .internal`
);
await page.type(
`${editorSelector} .internal`,
" and edited in Firefox"
);
// Disable editing mode.
await switchToFreeText(page, /* disable = */ true);
const oneToOne = Array.from(new Array(13).keys(), n => n + 2).concat(
Array.from(new Array(13).keys(), n => 13 - n)
);
for (const pageNumber of oneToOne) {
await scrollIntoView(
page,
`.page[data-page-number = "${pageNumber}"]`
);
}
// Check we've now a div containing the text.
await page.waitForSelector(
"[data-annotation-id='998R'] div.annotationContent"
);
const newDivText = await page.$eval(
"[data-annotation-id='998R'] div.annotationContent",
el => el.innerText.replaceAll("\xa0", " ")
);
expect(newDivText)
.withContext(`In ${browserName}`)
.toEqual("Hello World and edited in Firefox");
// Check that the canvas has nothing drawn at the annotation position.
await page.$eval(
"[data-annotation-id='998R']",
el => (el.hidden = true)
);
let editorPng = await page.screenshot({
clip: editorRect,
type: "png",
});
await page.$eval(
"[data-annotation-id='998R']",
el => (el.hidden = false)
);
let editorImage = PNG.sync.read(Buffer.from(editorPng));
expect(editorImage.data.every(x => x === 0xff))
.withContext(`In ${browserName}`)
.toBeTrue();
const oneToThirteen = Array.from(new Array(13).keys(), n => n + 2);
for (const pageNumber of oneToThirteen) {
await scrollIntoView(
page,
`.page[data-page-number = "${pageNumber}"]`
);
}
await switchToFreeText(page);
await kbUndo(page);
await waitForSerialized(page, 0);
// Disable editing mode.
await switchToFreeText(page, /* disable = */ true);
const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n);
const handlePromise = await createPromise(page, resolve => {
const callback = e => {
if (e.source.id === 1) {
window.PDFViewerApplication.eventBus.off(
"pagerendered",
callback
);
resolve();
}
};
window.PDFViewerApplication.eventBus.on("pagerendered", callback);
});
for (const pageNumber of thirteenToOne) {
await scrollIntoView(
page,
`.page[data-page-number = "${pageNumber}"]`
);
}
await awaitPromise(handlePromise);
editorPng = await page.screenshot({
clip: editorRect,
type: "png",
});
editorImage = PNG.sync.read(Buffer.from(editorPng));
expect(editorImage.data.every(x => x === 0xff))
.withContext(`In ${browserName}`)
.toBeFalse();
})
);
});
});
describe("Freetext and shift+enter", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must check that a freetext has the correct data", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const editorSelector = getEditorSelector(0);
const data = "Hello\nPDF.js\nWorld\n!!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
for (const line of data.split("\n")) {
await page.type(`${editorSelector} .internal`, line);
await page.keyboard.down("Shift");
await page.keyboard.press("Enter");
await page.keyboard.up("Shift");
}
await commit(page);
await waitForSerialized(page, 1);
const serialized = await getSerialized(page, x => x.value);
expect(serialized[0]).withContext(`In ${browserName}`).toEqual(data);
})
);
});
});
2024-10-19 02:19:46 +05:30
describe("Undo deletion popup has the expected behaviour", () => {
let pages;
const editorSelector = getEditorSelector(0);
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that deleting a FreeText editor can be undone using the undo button", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
2024-10-19 02:19:46 +05:30
await page.type(`${editorSelector} .internal`, data);
await commit(page);
2024-10-19 02:19:46 +05:30
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
await page.click("#editorUndoBarUndoButton");
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
})
);
});
it("must check that the undo deletion popup displays the correct message", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
const rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
2024-10-19 02:19:46 +05:30
await page.type(`${editorSelector} .internal`, data);
await commit(page);
2024-10-19 02:19:46 +05:30
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForFunction(() => {
const messageElement = document.querySelector(
"#editorUndoBarMessage"
);
return messageElement && messageElement.textContent.trim() !== "";
});
const message = await page.waitForSelector("#editorUndoBarMessage");
const messageText = await page.evaluate(
el => el.textContent,
message
);
expect(messageText).toContain("Text removed");
})
);
});
it("must check that the popup disappears when a new textbox is created", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToFreeText(page);
let rect = await getRect(page, ".annotationEditorLayer");
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.waitForSelector(editorSelector, { visible: true });
2024-10-19 02:19:46 +05:30
await page.type(`${editorSelector} .internal`, data);
await commit(page);
2024-10-19 02:19:46 +05:30
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar:not([hidden])");
rect = await getRect(page, ".annotationEditorLayer");
const secondEditorSelector = getEditorSelector(1);
2024-10-19 02:19:46 +05:30
const newData = "This is a new text box!";
await page.mouse.click(rect.x + 150, rect.y + 150);
await page.waitForSelector(secondEditorSelector, { visible: true });
await page.type(`${secondEditorSelector} .internal`, newData);
await commit(page);
2024-10-19 02:19:46 +05:30
await waitForSerialized(page, 1);
await page.waitForSelector("#editorUndoBar", { hidden: true });
})
);
});
});
});