pdf.js/test/integration/freetext_editor_spec.js

841 lines
27 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.
*/
const {
closePages,
getEditorSelector,
getSelectedEditors,
loadAndWait,
waitForEvent,
waitForSelectedEditor,
waitForStorageEntries,
} = require("./test_utils.js");
const copyPaste = async page => {
let promise = waitForEvent(page, "copy");
await page.keyboard.down("Control");
await page.keyboard.press("c");
await page.keyboard.up("Control");
await promise;
await page.waitForTimeout(10);
promise = waitForEvent(page, "paste");
await page.keyboard.down("Control");
await page.keyboard.press("v");
await page.keyboard.up("Control");
await promise;
};
describe("Editor", () => {
describe("FreeText", () => {
let pages;
beforeAll(async () => {
pages = await loadAndWait("aboutstacks.pdf", ".annotationEditorLayer");
});
afterAll(async () => {
await closePages(pages);
});
it("must write a string in a FreeText editor", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#editorFreeText");
const rect = await page.$eval(".annotationEditorLayer", el => {
// With Chrome something is wrong when serializing a DomRect,
// hence we extract the values and just return them.
const { x, y } = el.getBoundingClientRect();
return { x, y };
});
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.type(`${getEditorSelector(0)} .internal`, data);
const editorRect = await page.$eval(getEditorSelector(0), el => {
const { x, y, width, height } = el.getBoundingClientRect();
return {
x,
y,
width,
height,
};
});
// Commit.
await page.mouse.click(
editorRect.x,
editorRect.y + 2 * editorRect.height
);
await waitForSelectedEditor(page, getEditorSelector(0));
await waitForStorageEntries(page, 1);
const content = await page.$eval(getEditorSelector(0), 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) {
const editorRect = await page.$eval(getEditorSelector(0), el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
// Select the editor created previously.
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2
);
await waitForSelectedEditor(page, getEditorSelector(0));
await copyPaste(page);
await waitForStorageEntries(page, 2);
const content = await page.$eval(getEditorSelector(0), el =>
el.innerText.trimEnd()
);
let pastedContent = await page.$eval(getEditorSelector(1), el =>
el.innerText.trimEnd()
);
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
await copyPaste(page);
await waitForStorageEntries(page, 3);
pastedContent = await page.$eval(getEditorSelector(2), el =>
el.innerText.trimEnd()
);
expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
}
});
it("must clear all", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.keyboard.down("Control");
await page.keyboard.press("a");
await page.keyboard.up("Control");
await page.keyboard.down("Control");
await page.keyboard.press("Backspace");
await page.keyboard.up("Control");
for (const n of [0, 1, 2]) {
const hasEditor = await page.evaluate(sel => {
return !!document.querySelector(sel);
}, getEditorSelector(n));
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
}
await waitForStorageEntries(page, 0);
})
);
});
it("must check that a paste has been undone", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
const rect = await page.$eval(".annotationEditorLayer", el => {
const { x, y } = el.getBoundingClientRect();
return { x, y };
});
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.type(`${getEditorSelector(3)} .internal`, data);
const editorRect = await page.$eval(getEditorSelector(3), el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
// Commit.
await page.mouse.click(
editorRect.x,
editorRect.y + 2 * editorRect.height
);
// And select it again.
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2
);
await waitForSelectedEditor(page, getEditorSelector(3));
await copyPaste(page);
let hasEditor = await page.evaluate(sel => {
return !!document.querySelector(sel);
}, getEditorSelector(4));
expect(hasEditor).withContext(`In ${browserName}`).toEqual(true);
await page.keyboard.down("Control");
await page.keyboard.press("z");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
hasEditor = await page.evaluate(sel => {
return !!document.querySelector(sel);
}, getEditorSelector(4));
expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
for (let i = 0; i < 2; i++) {
const promise = waitForEvent(page, "paste");
await page.keyboard.down("Control");
await page.keyboard.press("v");
await page.keyboard.up("Control");
await promise;
await page.waitForTimeout(10);
}
let length = await page.evaluate(sel => {
return document.querySelectorAll(sel).length;
}, `${getEditorSelector(5)}, ${getEditorSelector(6)}`);
expect(length).withContext(`In ${browserName}`).toEqual(2);
for (let i = 0; i < 2; i++) {
await page.keyboard.down("Control");
await page.keyboard.press("z");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
}
length = await page.evaluate(sel => {
return document.querySelectorAll(sel).length;
}, `${getEditorSelector(5)}, ${getEditorSelector(6)}`);
expect(length).withContext(`In ${browserName}`).toEqual(0);
}
});
it("must check that aria-owns is correct", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const [stacksRect, oldAriaOwns] = 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);
const { x, y, width, height } = span.getBoundingClientRect();
return [
{ x, y, width, height },
span.getAttribute("aria-owns"),
];
}
}
return null;
}
);
expect(oldAriaOwns).withContext(`In ${browserName}`).toEqual(null);
const data = "Hello PDF.js World !!";
await page.mouse.click(
stacksRect.x + stacksRect.width + 1,
stacksRect.y + stacksRect.height / 2
);
await page.type(`${getEditorSelector(7)} .internal`, data);
// Commit.
await page.keyboard.press("Escape");
const ariaOwns = await page.$eval(".textLayer", el => {
const span = el.querySelector(`span[pdfjs="true"]`);
return span?.getAttribute("aria-owns") || null;
});
expect(ariaOwns.endsWith("_7-editor"))
.withContext(`In ${browserName}`)
.toEqual(true);
})
);
});
it("must check that right click doesn't select", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const rect = await page.$eval(".annotationEditorLayer", el => {
const { x, y } = el.getBoundingClientRect();
return { x, y };
});
await page.keyboard.down("Control");
await page.keyboard.press("a");
await page.keyboard.up("Control");
await page.keyboard.down("Control");
await page.keyboard.press("Backspace");
await page.keyboard.up("Control");
const data = "Hello PDF.js World !!";
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.type(`${getEditorSelector(8)} .internal`, data);
const editorRect = await page.$eval(getEditorSelector(8), el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
// Commit.
await page.keyboard.press("Escape");
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([8]);
await page.keyboard.press("Escape");
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([]);
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2
);
await waitForSelectedEditor(page, getEditorSelector(8));
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([8]);
// Escape.
await page.keyboard.press("Escape");
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([]);
// 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) {
const rect = await page.$eval(".annotationEditorLayer", el => {
const { x, y } = el.getBoundingClientRect();
return { x, y };
});
await page.keyboard.down("Control");
await page.keyboard.press("a");
await page.keyboard.up("Control");
await page.keyboard.down("Control");
await page.keyboard.press("Backspace");
await page.keyboard.up("Control");
await page.mouse.click(rect.x + 200, rect.y + 100);
for (let i = 0; i < 5; i++) {
await page.type(`${getEditorSelector(9)} .internal`, "A");
const editorRect = await page.$eval(getEditorSelector(9), el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
// Commit.
await page.mouse.click(
editorRect.x,
editorRect.y + 2 * editorRect.height
);
if (i < 4) {
// And select it again.
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
{ clickCount: 2 }
);
}
}
await page.keyboard.down("Control");
await page.keyboard.press("z");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
let text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
return el.innerText;
});
expect(text).withContext(`In ${browserName}`).toEqual("AAAA");
await page.keyboard.down("Control");
await page.keyboard.press("z");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
return el.innerText;
});
expect(text).withContext(`In ${browserName}`).toEqual("AAA");
await page.keyboard.down("Control");
await page.keyboard.press("y");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
return el.innerText;
});
expect(text).withContext(`In ${browserName}`).toEqual("AAAA");
for (let i = 0; i < 4; i++) {
await page.keyboard.down("Control");
await page.keyboard.press("z");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
}
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([]);
await page.keyboard.down("Control");
await page.keyboard.press("y");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
return el.innerText;
});
expect(text).withContext(`In ${browserName}`).toEqual("A");
// Add a new A.
const editorRect = await page.$eval(getEditorSelector(9), el => {
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
await page.mouse.click(
editorRect.x + editorRect.width / 2,
editorRect.y + editorRect.height / 2,
{ clickCount: 2 }
);
await page.type(`${getEditorSelector(9)} .internal`, "A");
// Commit.
await page.mouse.click(
editorRect.x,
editorRect.y + 2 * editorRect.height
);
text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
return el.innerText;
});
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 page.click("#editorFreeText");
const rect = await page.$eval(".annotationEditorLayer", el => {
// With Chrome something is wrong when serializing a DomRect,
// hence we extract the values and just return them.
const { x, y } = el.getBoundingClientRect();
return { x, y };
});
const editorCenters = [];
for (let i = 0; i < 4; i++) {
const data = `FreeText ${i}`;
await page.mouse.click(
rect.x + (i + 1) * 100,
rect.y + (i + 1) * 100
);
await page.type(`${getEditorSelector(i)} .internal`, data);
const editorRect = await page.$eval(getEditorSelector(i), el => {
const { x, y, width, height } = el.getBoundingClientRect();
return {
x,
y,
width,
height,
};
});
editorCenters.push({
x: editorRect.x + editorRect.width / 2,
y: editorRect.y + editorRect.height / 2,
});
// Commit.
await page.mouse.click(
editorRect.x,
editorRect.y + 2 * editorRect.height
);
}
await page.keyboard.down("Control");
await page.keyboard.press("a");
await page.keyboard.up("Control");
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([0, 1, 2, 3]);
await page.keyboard.down("Control");
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([0, 2, 3]);
await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([0, 3]);
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
await page.keyboard.up("Control");
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([0, 1, 3]);
await copyPaste(page);
// 0,1,3 are unselected and new pasted editors are selected.
expect(await getSelectedEditors(page))
.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);
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([2]);
await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([1]);
await page.keyboard.down("Control");
await page.mouse.click(editorCenters[3].x, editorCenters[3].y);
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([1, 3]);
await page.keyboard.up("Control");
// Delete 1 and 3.
await page.keyboard.press("Backspace");
await page.keyboard.down("Control");
await page.keyboard.press("a");
await page.keyboard.up("Control");
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([0, 2, 4, 5, 6]);
// Create an empty editor.
await page.mouse.click(rect.x + 700, rect.y + 100);
expect(await getSelectedEditors(page))
.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);
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([2]);
// Create an empty editor.
await page.mouse.click(rect.x + 700, rect.y + 100);
expect(await getSelectedEditors(page))
.withContext(`In ${browserName}`)
.toEqual([8]);
// Dismiss it.
await page.keyboard.press("Escape");
// Select all.
await page.keyboard.down("Control");
await page.keyboard.press("a");
await page.keyboard.up("Control");
// 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 getSelectedEditors(page))
.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 page.click("#editorFreeText");
let currentId = 0;
const expected = [];
const oneToFourteen = [...new Array(14).keys()].map(x => x + 1);
for (const pageNumber of oneToFourteen) {
const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
await page.evaluate(selector => {
const element = window.document.querySelector(selector);
element.scrollIntoView();
}, pageSelector);
const annotationLayerSelector = `${pageSelector} > .annotationEditorLayer`;
await page.waitForSelector(annotationLayerSelector, {
visible: true,
timeout: 0,
});
await page.waitForTimeout(50);
if (![1, 14].includes(pageNumber)) {
continue;
}
const rect = await page.$eval(annotationLayerSelector, el => {
// With Chrome something is wrong when serializing a DomRect,
// hence we extract the values and just return them.
const { x, y } = el.getBoundingClientRect();
return { x, y };
});
const data = `Hello PDF.js World !! on page ${pageNumber}`;
expected.push(data);
await page.mouse.click(rect.x + 100, rect.y + 100);
await page.type(`${getEditorSelector(currentId)} .internal`, data);
// Commit.
await page.keyboard.press("Escape");
await page.waitForTimeout(10);
await waitForSelectedEditor(page, getEditorSelector(currentId));
await waitForStorageEntries(page, currentId + 1);
const content = await page.$eval(getEditorSelector(currentId), el =>
el.innerText.trimEnd()
);
expect(content).withContext(`In ${browserName}`).toEqual(data);
currentId += 1;
await page.waitForTimeout(10);
}
const serialize = proprName =>
page.evaluate(
name =>
[
...window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.values(),
].map(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.
// Select all.
await page.keyboard.down("Control");
await page.keyboard.press("a");
await page.keyboard.up("Control");
await page.waitForTimeout(10);
page.evaluate(() => {
window.PDFViewerApplication.eventBus.dispatch(
"switchannotationeditorparams",
{
source: null,
type: /* AnnotationEditorParamsType.FREETEXT_SIZE */ 1,
value: 13,
}
);
});
await page.waitForTimeout(10);
expect(await serialize("fontSize"))
.withContext(`In ${browserName}`)
.toEqual([13, 13]);
// Change the colors for all the annotations.
page.evaluate(() => {
window.PDFViewerApplication.eventBus.dispatch(
"switchannotationeditorparams",
{
source: null,
type: /* AnnotationEditorParamsType.FREETEXT_COLOR */ 2,
value: "#FF0000",
}
);
});
await page.waitForTimeout(10);
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 page.click("#editorFreeText");
let currentId = 0;
for (let step = 0; step < 3; step++) {
const rect = await page.$eval(".annotationEditorLayer", el => {
// With Chrome something is wrong when serializing a DomRect,
// hence we extract the values and just return them.
const { x, y, width, height } = el.getBoundingClientRect();
return { x, y, width, height };
});
const data = `Hello ${step}`;
const x = rect.x + 0.1 * rect.width;
const y = rect.y + 0.1 * rect.height;
await page.mouse.click(x, y);
await page.type(`${getEditorSelector(currentId)} .internal`, data);
// Commit.
await page.keyboard.press("Escape");
await page.waitForTimeout(10);
await page.evaluate(() => {
document.getElementById("pageRotateCw").click();
});
currentId += 1;
await page.waitForTimeout(10);
}
const serialize = proprName =>
page.evaluate(
name =>
[
...window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.values(),
].map(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]);
})
);
});
});
});