pdf.js/test/integration/stamp_editor_spec.mjs
Tim van der Meij 603fd2524a
Fix intermittent failure in the viewer alert tests
It takes some time for the viewer alert to be updated after the editor
is committed, but the current tests don't await that and proceed too
fast to the viewer alert string assertion. This commit fixes the issue
by waiting for the expected viewer alert string to appear instead.
2025-07-18 20:56:02 +02:00

1828 lines
62 KiB
JavaScript

/* 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 {
applyFunctionToEditor,
awaitPromise,
clearEditors,
clearInput,
closePages,
copy,
copyToClipboard,
dragAndDrop,
getAnnotationSelector,
getEditorDimensions,
getEditors,
getEditorSelector,
getFirstSerialized,
getRect,
getSerialized,
isCanvasMonochrome,
kbBigMoveDown,
kbBigMoveRight,
kbUndo,
loadAndWait,
paste,
pasteFromClipboard,
scrollIntoView,
selectEditor,
serializeBitmapDimensions,
switchToEditor,
unselectEditor,
waitForAnnotationEditorLayer,
waitForAnnotationModeChanged,
waitForEntryInStorage,
waitForPageRendered,
waitForSelectedEditor,
waitForSerialized,
waitForTimeout,
} from "./test_utils.mjs";
import fs from "fs";
import path from "path";
import { PNG } from "pngjs";
const __dirname = import.meta.dirname;
const clearAll = clearEditors.bind(null, "stamp");
const waitForImage = async (page, selector) => {
await page.waitForSelector(`${selector} canvas`);
await page.waitForFunction(
sel => {
const canvas = document.querySelector(sel);
const data = canvas
.getContext("2d")
.getImageData(0, 0, canvas.width, canvas.height);
return data.data.some(x => x !== 0);
},
{},
`${selector} canvas`
);
await page.waitForSelector(`${selector} .altText`);
};
const copyImage = async (page, imagePath, selector) => {
const data = fs
.readFileSync(path.join(__dirname, imagePath))
.toString("base64");
await copyToClipboard(page, { "image/png": `data:image/png;base64,${data}` });
await pasteFromClipboard(page);
await waitForImage(page, selector);
};
async function waitForTranslation(page) {
return page.evaluate(async () => {
await new Promise(resolve => {
window.requestAnimationFrame(resolve);
});
});
}
const switchToStamp = switchToEditor.bind(null, "Stamp");
describe("Stamp Editor", () => {
describe("Basic operations", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", null, {
eventBusSetup: eventBus => {
eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
window.uiManager = uiManager;
});
},
});
});
afterEach(async () => {
await closePages(pages);
});
it("must load a PNG which is bigger than a page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
await page.click("#editorStampAddImage");
const input = await page.$("#stampEditorFileInput");
await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}`
);
const editorSelector = getEditorSelector(0);
await waitForImage(page, editorSelector);
await page.waitForFunction(
`document.getElementById("viewer-alert").textContent === "Image added"`
);
const { width } = await getEditorDimensions(page, editorSelector);
// The image is bigger than the page, so it has been scaled down to
// 75% of the page width.
expect(width).toEqual("75%");
const [bitmap] = await serializeBitmapDimensions(page);
expect(bitmap.width).toEqual(512);
expect(bitmap.height).toEqual(543);
})
);
});
it("must load a SVG", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
await page.click("#editorStampAddImage");
const input = await page.$("#stampEditorFileInput");
await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.svg")}`
);
const editorSelector = getEditorSelector(0);
await waitForImage(page, editorSelector);
const { width } = await getEditorDimensions(page, editorSelector);
expect(Math.round(parseFloat(width))).toEqual(40);
const [bitmap] = await serializeBitmapDimensions(page);
// The original size is 80x242 but to increase the resolution when it
// is rasterized we scale it up by 96 / 72
const ratio = await page.evaluate(
() => window.pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS
);
expect(Math.abs(bitmap.width - 242 * ratio) < 1).toBeTrue();
expect(Math.abs(bitmap.height - 80 * ratio) < 1).toBeTrue();
})
);
});
it("must load a SVG, delete it and undo", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
await page.click("#editorStampAddImage");
const input = await page.$("#stampEditorFileInput");
await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.svg")}`
);
const editorSelector = getEditorSelector(0);
await waitForImage(page, editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await kbUndo(page);
await waitForImage(page, editorSelector);
await waitForSerialized(page, 1);
await waitForSelectedEditor(page, editorSelector);
})
);
});
});
describe("Resize", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 50, {
eventBusSetup: eventBus => {
eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
window.uiManager = uiManager;
});
},
});
});
afterEach(async () => {
await closePages(pages);
});
it("must check that an added image stay within the page", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
const names = ["bottomLeft", "bottomRight", "topRight", "topLeft"];
for (let i = 0; i < 4; i++) {
if (i !== 0) {
await clearAll(page);
}
await page.click("#editorStampAddImage");
const input = await page.$("#stampEditorFileInput");
await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}`
);
const editorSelector = getEditorSelector(i);
await waitForImage(page, editorSelector);
await page.waitForSelector(`${editorSelector} .altText`);
for (let j = 0; j < 4; j++) {
await page.keyboard.press("Escape");
await page.waitForSelector(`${editorSelector} .resizers.hidden`);
const handle = await waitForAnnotationEditorLayer(page);
await page.evaluate(() => {
window.PDFViewerApplication.rotatePages(90);
});
await awaitPromise(handle);
await page.focus(".stampEditor");
await waitForSelectedEditor(page, editorSelector);
await page.waitForSelector(
`${editorSelector} .resizers:not(.hidden)`
);
const stampRect = await getRect(page, ".stampEditor");
const [name, cursor] = await page.evaluate(rect => {
const el = document.elementFromPoint(rect.x, rect.y);
const cornerName = Array.from(el.classList).find(
c => c !== "resizer"
);
return [cornerName, window.getComputedStyle(el).cursor];
}, stampRect);
expect(name).withContext(`In ${browserName}`).toEqual(names[j]);
expect(cursor)
.withContext(`In ${browserName}`)
.toEqual("nwse-resize");
}
const handle = await waitForAnnotationEditorLayer(page);
await page.evaluate(() => {
window.PDFViewerApplication.rotatePages(90);
});
await awaitPromise(handle);
}
})
);
});
it("must check that the opposite corner doesn't move", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await switchToStamp(page);
await page.click("#editorStampAddImage");
const input = await page.$("#stampEditorFileInput");
await input.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}`
);
const editorSelector = getEditorSelector(0);
await waitForImage(page, editorSelector);
await page.waitForSelector(`${editorSelector} .resizer.topLeft`);
const baseRect = await getRect(page, editorSelector);
const bRX = baseRect.x + baseRect.width;
const bRY = baseRect.y + baseRect.height;
await dragAndDrop(page, `${editorSelector} .resizer.topLeft`, [
[-10, -10],
[20, 20],
[-10, -10],
[20, 20],
]);
const newRect = await getRect(page, editorSelector);
const newBRX = newRect.x + newRect.width;
const newBRY = newRect.y + newRect.height;
expect(Math.abs(bRX - newBRX) <= 1)
.withContext(`In ${browserName}`)
.toBeTrue();
expect(Math.abs(bRY - newBRY) <= 1)
.withContext(`In ${browserName}`)
.toBeTrue();
})
);
});
});
describe("Alt text dialog", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 50);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the alt-text flow is correctly implemented", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
// Wait for the alt-text button to be visible.
const buttonSelector = `${editorSelector} button.altText`;
await page.waitForSelector(buttonSelector);
// Click on the alt-text button.
await page.click(buttonSelector);
// Wait for the alt-text dialog to be visible.
await page.waitForSelector("#altTextDialog", { visible: true });
// Click on the alt-text editor.
const textareaSelector = "#altTextDialog textarea";
await page.click(textareaSelector);
await page.type(textareaSelector, "Hello World");
// Click on save button.
const saveButtonSelector = "#altTextDialog #altTextSave";
await page.click(saveButtonSelector);
// Check that the canvas has an aria-describedby attribute.
await page.waitForSelector(`${editorSelector}[aria-describedby]`);
// Wait for the alt-text button to have the correct icon.
await page.waitForSelector(`${buttonSelector}.done`);
// Hover the button.
await page.hover(buttonSelector);
// Wait for the tooltip to be visible.
const tooltipSelector = `${buttonSelector} .tooltip`;
await page.waitForSelector(tooltipSelector, { visible: true });
let tooltipText = await page.evaluate(
sel => document.querySelector(`${sel}`).innerText,
tooltipSelector
);
expect(tooltipText).toEqual("Hello World");
// Now we change the alt-text and check that the tooltip is updated.
const longString = "a".repeat(512);
await page.click(buttonSelector);
await page.waitForSelector("#altTextDialog", { visible: true });
await page.evaluate(sel => {
document.querySelector(`${sel}`).value = "";
}, textareaSelector);
await page.click(textareaSelector);
await page.type(textareaSelector, longString);
await page.click(saveButtonSelector);
await page.waitForSelector(`${buttonSelector}.done`);
await page.hover(buttonSelector);
await page.waitForSelector(tooltipSelector, { visible: true });
tooltipText = await page.evaluate(
sel => document.querySelector(`${sel}`).innerText,
tooltipSelector
);
expect(tooltipText).toEqual(longString);
const dims = await page.evaluate(sel => {
const { width, height } = document
.querySelector(`${sel}`)
.getBoundingClientRect();
return { width, height };
}, tooltipSelector);
expect(dims.width / dims.height).toBeLessThan(2);
// Now we just check that cancel didn't change anything.
await page.click(buttonSelector);
await page.waitForSelector("#altTextDialog", { visible: true });
await page.evaluate(sel => {
document.querySelector(`${sel}`).value = "";
}, textareaSelector);
await page.click(textareaSelector);
await page.type(textareaSelector, "Hello PDF.js");
const cancelButtonSelector = "#altTextDialog #altTextCancel";
await page.click(cancelButtonSelector);
await page.waitForSelector(`${buttonSelector}.done`);
await page.hover(buttonSelector);
await page.waitForSelector(tooltipSelector, { visible: true });
tooltipText = await page.evaluate(
sel => document.querySelector(`${sel}`).innerText,
tooltipSelector
);
// The tooltip should still be longString.
expect(tooltipText).toEqual(longString);
// Now we switch to decorative.
await page.click(buttonSelector);
await page.waitForSelector("#altTextDialog", { visible: true });
const decorativeSelector = "#altTextDialog #decorativeButton";
await page.click(decorativeSelector);
await page.click(saveButtonSelector);
await page.waitForSelector(`${buttonSelector}.done`);
await page.hover(buttonSelector);
await page.waitForSelector(tooltipSelector, { visible: true });
tooltipText = await page.evaluate(
sel => document.querySelector(`${sel}`).innerText,
tooltipSelector
);
expect(tooltipText).toEqual("Marked as decorative");
// Now we switch back to non-decorative.
await page.click(buttonSelector);
await page.waitForSelector("#altTextDialog", { visible: true });
const descriptionSelector = "#altTextDialog #descriptionButton";
await page.click(descriptionSelector);
await page.click(saveButtonSelector);
await page.waitForSelector(`${buttonSelector}.done`);
await page.hover(buttonSelector);
await page.waitForSelector(tooltipSelector, { visible: true });
tooltipText = await page.evaluate(
sel => document.querySelector(`${sel}`).innerText,
tooltipSelector
);
expect(tooltipText).toEqual(longString);
// Now we remove the alt-text and check that the tooltip is removed.
await page.click(buttonSelector);
await page.waitForSelector("#altTextDialog", { visible: true });
await page.evaluate(sel => {
document.querySelector(`${sel}`).value = "";
}, textareaSelector);
await page.click(saveButtonSelector);
await page.waitForSelector(`${buttonSelector}:not(.done)`);
await page.hover(buttonSelector);
await page.evaluate(
sel => document.querySelector(sel) === null,
tooltipSelector
);
// We check that the alt-text button works correctly with the
// keyboard.
const handle = await page.evaluateHandle(sel => {
document.getElementById("viewerContainer").focus();
return [
new Promise(resolve => {
setTimeout(() => {
const el = document.querySelector(sel);
el.addEventListener("focus", resolve, { once: true });
el.focus({ focusVisible: true });
}, 0);
}),
];
}, buttonSelector);
await awaitPromise(handle);
await (browserName === "chrome"
? page.waitForSelector(`${buttonSelector}:focus`)
: page.waitForSelector(`${buttonSelector}:focus-visible`));
await page.keyboard.press("Enter");
await page.waitForSelector("#altTextDialog", { visible: true });
await page.keyboard.press("Escape");
await (browserName === "chrome"
? page.waitForSelector(`${buttonSelector}:focus`)
: page.waitForSelector(`${buttonSelector}:focus-visible`));
}
});
});
describe("Resize an image with the keyboard", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 50);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the dimensions change", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await selectEditor(page, editorSelector);
await page.waitForSelector(
`${editorSelector} .resizer.topLeft[tabindex="-1"]`
);
const getDims = async () => {
const [blX, blY, trX, trY] = await getFirstSerialized(
page,
x => x.rect
);
return [trX - blX, trY - blY];
};
const [width, height] = await getDims();
// Press Enter to enter in resize-with-keyboard mode.
await page.keyboard.press("Enter");
// The resizer must become keyboard focusable.
await page.waitForSelector(
`${editorSelector} .resizer.topLeft[tabindex="0"]`
);
let prevWidth = width;
let prevHeight = height;
const waitForDimsChange = async (w, h) => {
await page.waitForFunction(
(prevW, prevH) => {
const [x1, y1, x2, y2] =
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map
.values()
.next().value.rect;
const newWidth = x2 - x1;
const newHeight = y2 - y1;
return newWidth !== prevW || newHeight !== prevH;
},
{},
w,
h
);
};
for (let i = 0; i < 40; i++) {
await page.keyboard.press("ArrowLeft");
await waitForDimsChange(prevWidth, prevHeight);
[prevWidth, prevHeight] = await getDims();
}
let [newWidth, newHeight] = await getDims();
expect(newWidth > width + 30)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(newHeight > height + 30)
.withContext(`In ${browserName}`)
.toEqual(true);
for (let i = 0; i < 4; i++) {
await kbBigMoveRight(page);
await waitForDimsChange(prevWidth, prevHeight);
[prevWidth, prevHeight] = await getDims();
}
[newWidth, newHeight] = await getDims();
expect(Math.abs(newWidth - width) < 2)
.withContext(`In ${browserName}`)
.toEqual(true);
expect(Math.abs(newHeight - height) < 2)
.withContext(`In ${browserName}`)
.toEqual(true);
// Move the focus to the next resizer.
await page.keyboard.press("Tab");
await page.waitForFunction(
() => !!document.activeElement?.classList.contains("topMiddle")
);
for (let i = 0; i < 40; i++) {
await page.keyboard.press("ArrowUp");
await waitForDimsChange(prevWidth, prevHeight);
[prevWidth, prevHeight] = await getDims();
}
[, newHeight] = await getDims();
expect(newHeight > height + 50)
.withContext(`In ${browserName}`)
.toEqual(true);
for (let i = 0; i < 4; i++) {
await kbBigMoveDown(page);
await waitForDimsChange(prevWidth, prevHeight);
[prevWidth, prevHeight] = await getDims();
}
[, newHeight] = await getDims();
expect(Math.abs(newHeight - height) < 2)
.withContext(`In ${browserName}`)
.toEqual(true);
// Escape should remove the focus from the resizer.
await page.keyboard.press("Escape");
await page.waitForSelector(
`${editorSelector} .resizer.topLeft[tabindex="-1"]`
);
await page.waitForFunction(
() => !document.activeElement?.classList.contains("resizer")
);
}
});
});
describe("Copy/paste from a tab to an other", () => {
let pages1, pages2;
beforeEach(async () => {
pages1 = await loadAndWait("empty.pdf", ".annotationEditorLayer");
pages2 = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages1);
await closePages(pages2);
});
it("must check that the alt-text button is here when pasting in the second tab", async () => {
// Run sequentially to avoid clipboard issues.
for (let i = 0; i < pages1.length; i++) {
const [, page1] = pages1[i];
await page1.bringToFront();
await switchToStamp(page1);
const editorSelector = getEditorSelector(0);
await copyImage(page1, "../images/firefox_logo.png", editorSelector);
await copy(page1);
const [, page2] = pages2[i];
await page2.bringToFront();
await switchToStamp(page2);
await paste(page2);
await waitForImage(page2, editorSelector);
}
});
});
describe("Undo a stamp", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that a stamp can be undone", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await kbUndo(page);
await waitForImage(page, editorSelector);
await waitForSerialized(page, 1);
}
});
});
describe("Delete a stamp and undo it on another page", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that a stamp can be undone", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
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} canvas`);
}
});
});
describe("Delete a stamp, scroll and undo it", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that a stamp can be undone", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
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 waitForImage(page, editorSelector);
await waitForSerialized(page, 1);
}
});
});
describe("Resize a stamp", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that a resized stamp has its canvas at the right position", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
const serializedRect = await getFirstSerialized(page, x => x.rect);
const rect = await getRect(page, ".resizer.bottomRight");
const centerX = rect.x + rect.width / 2;
const centerY = rect.y + rect.height / 2;
await page.mouse.move(centerX, centerY);
await page.mouse.down();
await page.mouse.move(centerX - 500, centerY - 500);
await page.mouse.up();
await waitForEntryInStorage(
page,
"rect",
serializedRect,
(x, y) => x !== y
);
const canvasRect = await getRect(page, `${editorSelector} canvas`);
const stampRect = await getRect(page, editorSelector);
expect(
["x", "y", "width", "height"].every(
key => Math.abs(canvasRect[key] - stampRect[key]) <= 10
)
).toBeTrue();
}
});
});
describe("Add a stamp in odd spread mode", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"empty.pdf",
".annotationEditorLayer",
null,
null,
{
spreadModeOnLoad: 1,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the stamp has its canvas at the right position", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
const canvasRect = await getRect(page, `${editorSelector} canvas`);
const stampRect = await getRect(page, editorSelector);
expect(
["x", "y", "width", "height"].every(
key => Math.abs(canvasRect[key] - stampRect[key]) <= 10
)
).toBeTrue();
}
});
});
describe("Copy and paste a stamp with an alt text", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the pasted image has an alt text", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await applyFunctionToEditor(page, "pdfjs_internal_editor_0", editor => {
editor.altTextData = {
altText: "Hello World",
decorative: false,
};
});
await page.waitForSelector(`${editorSelector} .altText.done`);
await copy(page);
await paste(page);
await page.waitForSelector(`${getEditorSelector(1)} .altText.done`);
await waitForSerialized(page, 2);
const serialized = await getSerialized(
page,
x => x.accessibilityData?.alt
);
expect(serialized)
.withContext(`In ${browserName}`)
.toEqual(["Hello World", "Hello World"]);
}
});
});
describe("New alt-text flow", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"empty.pdf",
".annotationEditorLayer",
null,
{
eventBusSetup: eventBus => {
eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
window.uiManager = uiManager;
});
eventBus.on("reporttelemetry", ({ details }) => {
(window.telemetry ||= []).push(structuredClone(details));
});
},
},
{
enableAltText: true,
enableUpdatedAddImage: true,
enableGuessAltText: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check the new alt text flow (part 1)", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await switchToStamp(page);
// Add an image.
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
// Wait for the dialog to be visible.
await page.waitForSelector("#newAltTextDialog", { visible: true });
// Wait for the spinner to be visible.
await page.waitForSelector("#newAltTextDescriptionContainer.loading");
// Check we've the disclaimer.
await page.waitForSelector("#newAltTextDisclaimer", { visible: true });
// Check that the dialog has the correct title: "Edit..."
await page.waitForFunction(
"document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
);
// Check that AI guessed the correct alt text.
await page.waitForFunction(
`document.getElementById("newAltTextDescriptionTextarea").value ===
"Fake alt text."`
);
// Check that the dialog has the correct title: "Edit..."
await page.waitForFunction(
"document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
);
// Check we've the disclaimer.
await page.waitForSelector("#newAltTextDisclaimer", { visible: true });
// Clear the input and check that the title changes to "Add..."
await clearInput(
page,
"#newAltTextDescriptionTextarea",
/* waitForInputEvent = */ true
);
await page.waitForFunction(
"document.getElementById('newAltTextTitle').textContent.startsWith('Add')"
);
// Check we haven't the disclaimer.
await page.waitForSelector("#newAltTextDisclaimer", { visible: false });
// Add a new alt text and check that the title changes to "Edit..."
await page.type("#newAltTextDescriptionTextarea", "Hello World");
await page.waitForFunction(
"document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
);
// Check we haven't the disclaimer after the modification.
await page.waitForSelector("#newAltTextDisclaimer", { visible: false });
// Click on the Not Now button.
await page.click("#newAltTextNotNow");
await page.waitForSelector("#newAltTextDialog", { visible: false });
await waitForSelectedEditor(page, editorSelector);
// Wait for the alt-text button to be visible.
const buttonSelector = `${editorSelector} button.altText.new`;
await page.waitForSelector(buttonSelector, { visible: true });
await waitForTranslation(page);
// Check the text in the button.
let text = await page.evaluate(
sel => document.querySelector(sel).textContent,
buttonSelector
);
let ariaLabel = await page.evaluate(
sel => document.querySelector(sel).getAttribute("aria-label"),
buttonSelector
);
expect(text === ariaLabel && text)
.withContext(`In ${browserName}`)
.toEqual("Review alt text");
// Unselect and select the editor and check that the badge is visible.
await unselectEditor(page, editorSelector);
await page.waitForSelector(".editToolbar", { visible: false });
await page.waitForSelector(".noAltTextBadge", { visible: true });
await page.evaluate(() => {
window.uiManager.selectAll();
});
await waitForSelectedEditor(page, editorSelector);
await page.waitForSelector(".editToolbar", { visible: true });
await page.waitForSelector(".noAltTextBadge", { visible: false });
// Click on the Review button.
await page.click(buttonSelector);
await page.waitForSelector("#newAltTextDialog", { visible: true });
// Check that the dialog has the correct title: "Edit..."
await page.waitForFunction(
"document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
);
// Click on create automatically toggle button.
await page.click("#newAltTextCreateAutomaticallyButton");
await clearInput(
page,
"#newAltTextDescriptionTextarea",
/* waitForInputEvent = */ true
);
// Save the empty text.
await page.click("#newAltTextSave");
await page.waitForSelector("#newAltTextDialog", { visible: false });
await waitForSelectedEditor(page, editorSelector);
await page.waitForSelector(buttonSelector, { visible: true });
await waitForTranslation(page);
// Check the text in the button.
text = await page.evaluate(
sel => document.querySelector(sel).textContent,
buttonSelector
);
ariaLabel = await page.evaluate(
sel => document.querySelector(sel).getAttribute("aria-label"),
buttonSelector
);
expect(text === ariaLabel && text)
.withContext(`In ${browserName}`)
.toEqual("Missing alt text");
// Unselect and select the editor and check that the badge is visible.
await unselectEditor(page, editorSelector);
await page.waitForSelector(".editToolbar", { visible: false });
await page.waitForSelector(".noAltTextBadge", { visible: true });
await page.evaluate(() => {
window.uiManager.selectAll();
});
await waitForSelectedEditor(page, editorSelector);
await page.waitForSelector(".editToolbar", { visible: true });
await page.waitForSelector(".noAltTextBadge", { visible: false });
// Click on the Review button.
await page.click(buttonSelector);
await page.waitForSelector("#newAltTextDialog", { visible: true });
await page.waitForFunction(
"document.getElementById('newAltTextTitle').textContent.startsWith('Add')"
);
// Add a new alt text and check that the title changes to "Edit..."
await page.type("#newAltTextDescriptionTextarea", "Hello World");
await page.waitForFunction(
"document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
);
// Click on the Save button.
await page.click("#newAltTextSave");
await page.waitForSelector("#newAltTextDialog", { visible: false });
await waitForTranslation(page);
// Check the text in the button.
text = await page.evaluate(
sel => document.querySelector(sel).firstChild.textContent,
buttonSelector
);
ariaLabel = await page.evaluate(
sel => document.querySelector(sel).getAttribute("aria-label"),
buttonSelector
);
expect(text === ariaLabel && text)
.withContext(`In ${browserName}`)
.toEqual("Alt text added");
await page.hover(buttonSelector);
// Wait for the tooltip to be visible.
const tooltipSelector = `${buttonSelector} .tooltip`;
await page.waitForSelector(tooltipSelector, { visible: true });
const tooltipText = await page.evaluate(
sel => document.querySelector(`${sel}`).textContent,
tooltipSelector
);
expect(tooltipText).toEqual("Hello World");
// Click on the Review button.
await page.click(buttonSelector);
await page.waitForSelector("#newAltTextDialog", { visible: true });
await page.click("#newAltTextCreateAutomaticallyButton");
await page.click("#newAltTextCancel");
await page.waitForSelector("#newAltTextDialog", { visible: false });
}
});
it("must check the new alt text flow (part 2)", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
// Add an image.
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
// Wait for the dialog to be visible.
await page.waitForSelector("#newAltTextDialog", { visible: true });
// Wait for the spinner to be visible.
await page.waitForSelector("#newAltTextDescriptionContainer.loading");
// Check we've the disclaimer.
await page.waitForSelector("#newAltTextDisclaimer", { visible: true });
// Click in the textarea in order to stop the guessing.
await page.click("#newAltTextDescriptionTextarea");
await page.waitForFunction(() =>
document
.getElementById("newAltTextTitle")
.textContent.startsWith("Add ")
);
// Check we haven't the disclaimer.
await page.waitForSelector("#newAltTextDisclaimer", { visible: false });
// Click on the Not Now button.
await page.click("#newAltTextNotNow");
await page.waitForSelector("#newAltTextDialog", { visible: false });
}
});
it("must check the new alt text flow (part 3)", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await page.evaluate(() => {
window.PDFViewerApplication.mlManager.enableAltTextModelDownload = false;
});
await switchToStamp(page);
// Add an image.
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
// Wait for the dialog to be visible.
await page.waitForSelector("#newAltTextDialog", { visible: true });
// Check we haven't the disclaimer.
await page.waitForSelector("#newAltTextDisclaimer[hidden]");
}
});
it("must check that the data in telemetry are correct", async () => {
// Run sequentially to avoid clipboard issues.
for (const [browserName, page] of pages) {
await page.evaluate(() => {
window.PDFViewerApplication.mlManager.enableAltTextModelDownload = true;
});
await switchToStamp(page);
// Add an image.
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
// Wait for the dialog to be visible.
await page.waitForSelector("#newAltTextDialog", { visible: true });
// Check that AI guessed the correct alt text.
await page.waitForFunction(
`document.getElementById("newAltTextDescriptionTextarea").value ===
"Fake alt text."`
);
// Clear the input and check that the title changes to "Add..."
await clearInput(
page,
"#newAltTextDescriptionTextarea",
/* waitForInputEvent = */ true
);
// Save the empty text.
await page.click("#newAltTextSave");
await page.waitForSelector("#newAltTextDialog", { visible: false });
// Get the telemetry data and clean.
let telemetry = await page.evaluate(() => {
const tel = window.telemetry;
window.telemetry = [];
return tel;
});
let saveTelemetry = telemetry.find(
details => details.data.action === "pdfjs.image.alt_text.user_edit"
);
expect(saveTelemetry.data.data)
.withContext(`In ${browserName}`)
.toEqual({
total_words: 3,
words_removed: 3,
words_added: 0,
});
// Click on the Review button.
const buttonSelector = `${editorSelector} button.altText.new`;
await page.waitForSelector(buttonSelector, { visible: true });
await page.click(buttonSelector);
await page.waitForSelector("#newAltTextDialog", { visible: true });
// Add a new alt text and check that the title changes to "Edit..."
await page.type("#newAltTextDescriptionTextarea", "Fake text alt foo.");
// Save the empty text.
await page.click("#newAltTextSave");
await page.waitForSelector("#newAltTextDialog", { visible: false });
telemetry = await page.evaluate(() => window.telemetry);
saveTelemetry = telemetry.find(
details => details.data.action === "pdfjs.image.alt_text.user_edit"
);
expect(saveTelemetry.data.data)
.withContext(`In ${browserName}`)
.toEqual({
total_words: 3,
words_removed: 0,
words_added: 1,
});
}
});
});
describe("New alt-text flow (bug 1920515)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"empty.pdf",
".annotationEditorLayer",
null,
{
eventBusSetup: eventBus => {
eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
window.uiManager = uiManager;
});
},
},
{
enableAltText: false,
enableFakeMLManager: false,
enableUpdatedAddImage: true,
enableGuessAltText: true,
}
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the toggle button isn't displayed when there is no AI", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
// Add an image.
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
// Wait for the dialog to be visible.
await page.waitForSelector("#newAltTextDialog.noAi", { visible: true });
// enableFakeMLManager is false, so it means that we don't have ML but
// we're using the new flow, hence we don't want to have the toggle
// button.
await page.waitForSelector("#newAltTextCreateAutomatically", {
hidden: true,
});
}
});
});
describe("No auto-resize", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 67);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that a stamp editor isn't resizing itself", async () => {
// Run sequentially to avoid clipboard issues.
const editorSelector = getEditorSelector(0);
for (const [, page] of pages) {
await switchToStamp(page);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
}
await Promise.all(
pages.map(async ([browserName, page]) => {
const getDims = () =>
page.evaluate(sel => {
const bbox = document.querySelector(sel).getBoundingClientRect();
return `${bbox.width}::${bbox.height}`;
}, editorSelector);
const initialDims = await getDims();
for (let i = 0; i < 50; i++) {
// We want to make sure that the editor doesn't resize itself, so we
// check every 10ms that the dimensions are the same.
// eslint-disable-next-line no-restricted-syntax
await waitForTimeout(10);
const dims = await getDims();
expect(dims).withContext(`In ${browserName}`).toEqual(initialDims);
}
})
);
});
});
describe("A stamp mustn't be on top of the secondary toolbar", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 600);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that a stamp editor isn't on top of the secondary toolbar", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/red.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
}
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { visible: true });
const secondary = await page.$("#secondaryToolbar");
const png = await secondary.screenshot({ type: "png" });
const secondaryImage = PNG.sync.read(Buffer.from(png));
const buffer = new Uint32Array(secondaryImage.data.buffer);
expect(buffer.every(x => x === 0xff0000ff))
.withContext(`In ${browserName}`)
.toBeFalse();
})
);
});
});
describe("Stamp (move existing)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("stamps.pdf", getAnnotationSelector("25R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must move an annotation", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const modeChangedHandle = await waitForAnnotationModeChanged(page);
await page.click(getAnnotationSelector("25R"), { count: 2 });
await awaitPromise(modeChangedHandle);
const editorSelector = getEditorSelector(0);
await waitForSelectedEditor(page, editorSelector);
const editorIds = await getEditors(page, "stamp");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);
// 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.
await selectEditor(page, editorSelector);
await dragAndDrop(page, editorSelector, [[100, 100]]);
await waitForSerialized(page, 1);
})
);
});
});
describe("Stamp (change alt-text)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("stamps.pdf", getAnnotationSelector("58R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must update an existing alt-text", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const modeChangedHandle = await waitForAnnotationModeChanged(page);
await page.click(getAnnotationSelector("58R"), { count: 2 });
await awaitPromise(modeChangedHandle);
const editorSelector = getEditorSelector(4);
await waitForSelectedEditor(page, editorSelector);
const editorIds = await getEditors(page, "stamp");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);
await page.click(`${editorSelector} button.altText`);
await page.waitForSelector("#altTextDialog", { visible: true });
const textareaSelector = "#altTextDialog textarea";
await page.waitForFunction(
sel => document.querySelector(sel).value !== "",
{},
textareaSelector
);
const altText = await page.evaluate(
sel => document.querySelector(sel).value,
textareaSelector
);
expect(altText).toEqual("An elephant");
await page.evaluate(sel => {
document.querySelector(sel).value = "";
}, textareaSelector);
await page.click(textareaSelector);
await page.type(textareaSelector, "Hello World");
// 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([]);
const saveButtonSelector = "#altTextDialog #altTextSave";
await page.click(saveButtonSelector);
await waitForSerialized(page, 1);
})
);
});
});
describe("Stamp (delete existing and undo)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("stamps.pdf", getAnnotationSelector("37R"));
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the annotation is correctly restored", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const modeChangedHandle = await waitForAnnotationModeChanged(page);
await page.click(getAnnotationSelector("37R"), { count: 2 });
await awaitPromise(modeChangedHandle);
const editorSelector = getEditorSelector(2);
await waitForSelectedEditor(page, editorSelector);
const editorIds = await getEditors(page, "stamp");
expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);
// All the current annotations should be serialized as null objects
// because they haven't been edited yet.
let serialized = await getSerialized(page);
expect(serialized).withContext(`In ${browserName}`).toEqual([]);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 1);
serialized = await getSerialized(page);
expect(serialized)
.withContext(`In ${browserName}`)
.toEqual([
{ id: "37R", deleted: true, pageIndex: 0, popupRef: "44R" },
]);
await kbUndo(page);
await waitForSerialized(page, 0);
await waitForSelectedEditor(page, editorSelector);
})
);
});
});
describe("Drag a stamp annotation and click on a touchscreen", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the annotation isn't unselected when an other finger taps on the screen", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
const stampRect = await getRect(page, editorSelector);
await page.touchscreen.tap(stampRect.x + 10, stampRect.y + 10);
await waitForSelectedEditor(page, editorSelector);
await page.touchscreen.touchStart(stampRect.x + 10, stampRect.y + 10);
await page.touchscreen.touchMove(stampRect.x + 20, stampRect.y + 20);
await page.touchscreen.tap(stampRect.x - 10, stampRect.y - 10);
await page.touchscreen.touchEnd();
await waitForSelectedEditor(page, editorSelector);
}
});
});
describe("Undo deletion popup has the expected behaviour", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that deleting an image can be undone using the undo button", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar", { visible: true });
await page.waitForSelector("#editorUndoBarUndoButton", {
visible: true,
});
await page.click("#editorUndoBarUndoButton");
await waitForSerialized(page, 1);
await page.waitForSelector(editorSelector);
await page.waitForSelector(`${editorSelector} canvas`);
}
});
it("must check that the undo deletion popup displays the correct message", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
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("Image removed");
}
});
it("must check that the popup disappears when a new image is inserted", async () => {
// Run sequentially to avoid clipboard issues.
for (const [, page] of pages) {
await switchToStamp(page);
const editorSelector = getEditorSelector(0);
await copyImage(page, "../images/firefox_logo.png", editorSelector);
await page.waitForSelector(editorSelector);
await waitForSerialized(page, 1);
await page.waitForSelector(`${editorSelector} button.delete`);
await page.click(`${editorSelector} button.delete`);
await waitForSerialized(page, 0);
await page.waitForSelector("#editorUndoBar", { visible: true });
await page.click("#editorStampAddImage");
const newInput = await page.$("#stampEditorFileInput");
await newInput.uploadFile(
`${path.join(__dirname, "../images/firefox_logo.png")}`
);
await waitForImage(page, getEditorSelector(1));
await waitForSerialized(page, 1);
await page.waitForSelector("#editorUndoBar", { hidden: true });
}
});
});
describe("Switch to edit mode a pdf with an existing stamp annotation on an invisible and rendered page", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue19239.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must move on the second page", async () => {
await Promise.all(
pages.map(async ([, page]) => {
const pageOneSelector = `.page[data-page-number = "1"]`;
const pageTwoSelector = `.page[data-page-number = "2"]`;
await scrollIntoView(page, pageTwoSelector);
await page.waitForSelector(pageOneSelector, { visible: false });
await switchToStamp(page);
await scrollIntoView(page, pageOneSelector);
await page.waitForSelector(
`${pageOneSelector} .annotationEditorLayer canvas`,
{ visible: true }
);
})
);
});
});
describe("Switch to edit mode a pdf with an existing stamp annotation on an invisible and unrendered page", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue19239.pdf", ".annotationEditorLayer");
});
afterEach(async () => {
await closePages(pages);
});
it("must move on the last page", async () => {
await Promise.all(
pages.map(async ([, page]) => {
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 switchToStamp(page);
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(
`.page[data-page-number = "1"] .annotationEditorLayer canvas`,
{ visible: true }
);
})
);
});
});
describe("Switch to edit mode by double clicking on an existing stamp annotation", () => {
const annotationSelector = getAnnotationSelector("999R");
let pages;
beforeEach(async () => {
pages = await loadAndWait("issue19239.pdf", annotationSelector);
});
afterEach(async () => {
await closePages(pages);
});
it("must switch to edit mode", async () => {
await Promise.all(
pages.map(async ([, page]) => {
await page.waitForSelector(annotationSelector);
await scrollIntoView(page, annotationSelector);
await page.click(annotationSelector, { count: 2 });
await page.waitForFunction(() =>
document
.querySelector(".annotationEditorLayer")
.classList.contains("stampEditing")
);
})
);
});
});
describe("Switch to edit mode, zoom and check that the non-editable stamp is still there", () => {
const annotationSelector = getAnnotationSelector("14R");
let pages;
beforeEach(async () => {
pages = await loadAndWait("red_stamp.pdf", annotationSelector, 20);
});
afterEach(async () => {
await closePages(pages);
});
it("must check if the canvas is still red", async () => {
await Promise.all(
pages.map(async ([, page]) => {
expect(
await isCanvasMonochrome(page, 1, null, 0xff0000ff)
).toBeTrue();
await switchToStamp(page);
expect(
await isCanvasMonochrome(page, 1, null, 0xff0000ff)
).toBeTrue();
const rectPage = await getRect(
page,
`.page[data-page-number = "1"] .annotationEditorLayer`
);
const handle = await waitForPageRendered(page, 1);
const originX = rectPage.x + rectPage.width / 2;
const originY = rectPage.y + rectPage.height / 2;
await page.evaluate(
origin => {
window.PDFViewerApplication.pdfViewer.increaseScale({
scaleFactor: 2,
origin,
});
},
[originX, originY]
);
await awaitPromise(handle);
expect(
await isCanvasMonochrome(page, 1, null, 0xff0000ff)
).toBeTrue();
})
);
});
});
});