mirror of
https://github.com/mozilla/pdf.js.git
synced 2026-05-31 23:31:02 +02:00
The custom solution for obtaining the bounding box of a given element that we have now was necessary during the original introduction of the integration tests because at the time the `ElementHandle.boundingBox()` API in Puppeteer didn't work correctly in Chrome. However, `getRect`, where this is used, is a hot utility function because most tests call it multiple times, either directly or indirectly via other utility functions, and it turns out that the approach we use is slower than the native `ElementHandle.boundingBox()` API. Fortunately, most likely after a combination of Chrome/Puppeteer updates and the conversion to the formalized WebDriver BiDi protocol the custom solution is no longer necessary because all tests pass without it too, so this commit converts `getRect` to use `ElementHandle.boundingBox()` instead to speed up the tests.
1200 lines
32 KiB
JavaScript
1200 lines
32 KiB
JavaScript
/* Copyright 2020 Mozilla Foundation
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
import { mergeCoverageIntoGlobal } from "../coverage_utils.js";
|
|
import os from "os";
|
|
|
|
const isMac = os.platform() === "darwin";
|
|
|
|
function loadAndWait(filename, selector, zoom, setups, options, viewport) {
|
|
return Promise.all(
|
|
global.integrationSessions.map(async session => {
|
|
const page = await session.browser.newPage();
|
|
|
|
if (viewport) {
|
|
await page.setViewport(viewport);
|
|
}
|
|
|
|
// In order to avoid errors because of checks which depend on
|
|
// a locale.
|
|
await page.evaluateOnNewDocument(() => {
|
|
Object.defineProperty(navigator, "language", {
|
|
get() {
|
|
return "en-US";
|
|
},
|
|
});
|
|
Object.defineProperty(navigator, "languages", {
|
|
get() {
|
|
return ["en-US", "en"];
|
|
},
|
|
});
|
|
});
|
|
|
|
let app_options = "";
|
|
if (options) {
|
|
const optionsObject =
|
|
typeof options === "function"
|
|
? await options(page, session.name)
|
|
: options;
|
|
|
|
// Options must be handled in app.js::_parseHashParams.
|
|
for (const [key, value] of Object.entries(optionsObject || {})) {
|
|
app_options += `&${key}=${encodeURIComponent(value)}`;
|
|
}
|
|
}
|
|
|
|
const fileParam = filename.startsWith("http")
|
|
? filename
|
|
: `/test/pdfs/${filename}`;
|
|
const url = `${global.integrationBaseUrl}?file=${fileParam}#zoom=${zoom ?? "page-fit"}${app_options}`;
|
|
|
|
if (setups) {
|
|
// page.evaluateOnNewDocument allows us to run code before the
|
|
// first js script is executed.
|
|
// The idea here is to set up some setters for PDFViewerApplication
|
|
// and EventBus, so we can inject some code to do whatever we want
|
|
// soon enough especially before the first event in the eventBus is
|
|
// dispatched.
|
|
const { prePageSetup, appSetup, earlySetup, eventBusSetup } = setups;
|
|
await prePageSetup?.(page);
|
|
if (earlySetup || appSetup || eventBusSetup) {
|
|
await page.evaluateOnNewDocument(
|
|
(eaSetup, aSetup, evSetup) => {
|
|
if (eaSetup) {
|
|
// eslint-disable-next-line no-eval
|
|
eval(`(${eaSetup})`)();
|
|
}
|
|
let app;
|
|
let eventBus;
|
|
Object.defineProperty(window, "PDFViewerApplication", {
|
|
get() {
|
|
return app;
|
|
},
|
|
set(newValue) {
|
|
app = newValue;
|
|
if (aSetup) {
|
|
// eslint-disable-next-line no-eval
|
|
eval(`(${aSetup})`)(app);
|
|
}
|
|
Object.defineProperty(app, "eventBus", {
|
|
get() {
|
|
return eventBus;
|
|
},
|
|
set(newV) {
|
|
eventBus = newV;
|
|
if (evSetup) {
|
|
// eslint-disable-next-line no-eval
|
|
eval(`(${evSetup})`)(eventBus);
|
|
}
|
|
},
|
|
});
|
|
},
|
|
});
|
|
},
|
|
earlySetup?.toString(),
|
|
appSetup?.toString(),
|
|
eventBusSetup?.toString()
|
|
);
|
|
}
|
|
}
|
|
|
|
await page.goto(url);
|
|
await setups?.postPageSetup?.(page);
|
|
|
|
await page.bringToFront();
|
|
if (selector) {
|
|
await page.waitForSelector(selector, {
|
|
timeout: 0,
|
|
});
|
|
}
|
|
return [session.name, page];
|
|
})
|
|
);
|
|
}
|
|
|
|
function createPromise(page, callback) {
|
|
return page.evaluateHandle(
|
|
// eslint-disable-next-line no-eval
|
|
cb => [new Promise(eval(`(${cb})`))],
|
|
callback.toString()
|
|
);
|
|
}
|
|
|
|
function createPromiseWithArgs(page, callback, args) {
|
|
return page.evaluateHandle(
|
|
// eslint-disable-next-line no-eval, no-shadow
|
|
(cb, args) => [new Promise(eval(`(${cb})`))],
|
|
callback.toString(),
|
|
args
|
|
);
|
|
}
|
|
|
|
function awaitPromise(promise) {
|
|
return promise.evaluate(([p]) => p);
|
|
}
|
|
|
|
function closePages(pages) {
|
|
return Promise.all(pages.map(([_, page]) => closeSinglePage(page)));
|
|
}
|
|
|
|
async function closeSinglePage(page) {
|
|
const coverage = await page.evaluate(async () => {
|
|
// Collect coverage data from the worker before the document is closed.
|
|
let workerCoverage = null;
|
|
const handler =
|
|
window.PDFViewerApplication.pdfDocument?._transport?.messageHandler;
|
|
if (handler) {
|
|
try {
|
|
workerCoverage = await handler.sendWithPromise(
|
|
"GetWorkerCoverage",
|
|
null
|
|
);
|
|
} catch {}
|
|
}
|
|
|
|
// Close the viewer gracefully, and clear local storage to avoid state
|
|
// leaking from one test to another.
|
|
await window.PDFViewerApplication.testingClose();
|
|
window.localStorage.clear();
|
|
|
|
// Serialize the coverage data to a JSON string because that is a lot
|
|
// faster/cheaper to transfer from the browser to Node.js over the WebDriver
|
|
// BiDi protocol, otherwise Puppeteer's (significantly slower) serialization
|
|
// logic kicks in (see https://github.com/puppeteer/puppeteer/issues/2427).
|
|
return {
|
|
page: window.__coverage__ ? JSON.stringify(window.__coverage__) : null,
|
|
worker: workerCoverage ? JSON.stringify(workerCoverage) : null,
|
|
};
|
|
});
|
|
|
|
if (coverage.page) {
|
|
mergeCoverageIntoGlobal(JSON.parse(coverage.page));
|
|
}
|
|
if (coverage.worker) {
|
|
mergeCoverageIntoGlobal(JSON.parse(coverage.worker));
|
|
}
|
|
|
|
await page.close({ runBeforeUnload: false });
|
|
}
|
|
|
|
async function waitForSandboxTrip(page) {
|
|
const handle = await page.evaluateHandle(() => [
|
|
new Promise(resolve => {
|
|
window.addEventListener("sandboxtripend", resolve, { once: true });
|
|
window.PDFViewerApplication.pdfScriptingManager.sandboxTrip();
|
|
}),
|
|
]);
|
|
await awaitPromise(handle);
|
|
}
|
|
|
|
async function waitForDOMMutation(page, callback) {
|
|
return page.evaluateHandle(
|
|
cb => [
|
|
new Promise(resolve => {
|
|
const mutationObserver = new MutationObserver(mutationList => {
|
|
// eslint-disable-next-line no-eval
|
|
if (eval(`(${cb})`)(mutationList)) {
|
|
mutationObserver.disconnect();
|
|
resolve();
|
|
}
|
|
});
|
|
mutationObserver.observe(document, { childList: true, subtree: true });
|
|
}),
|
|
],
|
|
callback.toString()
|
|
);
|
|
}
|
|
|
|
function waitForTimeout(milliseconds) {
|
|
/**
|
|
* Wait for the given number of milliseconds.
|
|
*
|
|
* Note that waiting for an arbitrary time in tests is discouraged because it
|
|
* can easily cause intermittent failures, which is why this functionality is
|
|
* no longer provided by Puppeteer 22+ and we have to implement it ourselves
|
|
* for the remaining callers in the integration tests. We should avoid
|
|
* creating new usages of this function; instead please refer to the better
|
|
* alternatives at https://github.com/puppeteer/puppeteer/pull/11780.
|
|
*/
|
|
return new Promise(resolve => {
|
|
setTimeout(resolve, milliseconds);
|
|
});
|
|
}
|
|
|
|
async function clearInput(page, selector, waitForInputEvent = false) {
|
|
const action = async () => {
|
|
await page.click(selector);
|
|
await kbSelectAll(page);
|
|
await page.keyboard.press("Backspace");
|
|
await page.waitForFunction(
|
|
`document.querySelector('${selector}').value === ""`
|
|
);
|
|
};
|
|
return waitForInputEvent
|
|
? waitForEvent({
|
|
page,
|
|
eventName: "input",
|
|
action,
|
|
selector,
|
|
})
|
|
: action();
|
|
}
|
|
|
|
async function waitAndClick(page, selector, clickOptions = {}) {
|
|
await page.waitForSelector(selector, { visible: true });
|
|
await page.click(selector, clickOptions);
|
|
}
|
|
|
|
function waitForPointerUp(page) {
|
|
return createPromise(page, resolve => {
|
|
window.addEventListener("pointerup", resolve, { once: true });
|
|
});
|
|
}
|
|
|
|
function getSelector(id) {
|
|
return `[data-element-id="${id}"]`;
|
|
}
|
|
|
|
async function getRect(page, selector) {
|
|
await page.waitForSelector(selector, { visible: true });
|
|
return (await page.$(selector)).boundingBox();
|
|
}
|
|
|
|
function getQuerySelector(id) {
|
|
return `document.querySelector('${getSelector(id)}')`;
|
|
}
|
|
|
|
function getComputedStyleSelector(id) {
|
|
return `getComputedStyle(${getQuerySelector(id)})`;
|
|
}
|
|
|
|
function getEditorSelector(n) {
|
|
return `#pdfjs_internal_editor_${n}`;
|
|
}
|
|
|
|
function getAnnotationSelector(id) {
|
|
return `[data-annotation-id="${id}"]`;
|
|
}
|
|
|
|
function getThumbnailSelector(pageNumber) {
|
|
return `.thumbnailImageContainer[data-l10n-args^='{"page":${pageNumber}']`;
|
|
}
|
|
|
|
async function getSpanRectFromText(page, pageNumber, text) {
|
|
await page.waitForSelector(
|
|
`.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
|
|
);
|
|
return page.evaluate(
|
|
(number, content) => {
|
|
for (const el of document.querySelectorAll(
|
|
`.page[data-page-number="${number}"] > .textLayer span:not(:has(> span))`
|
|
)) {
|
|
if (el.textContent === content) {
|
|
const { x, y, width, height } = el.getBoundingClientRect();
|
|
return { x, y, width, height };
|
|
}
|
|
}
|
|
return null;
|
|
},
|
|
pageNumber,
|
|
text
|
|
);
|
|
}
|
|
|
|
async function waitForEvent({
|
|
page,
|
|
eventName,
|
|
action,
|
|
selector = null,
|
|
validator = null,
|
|
timeout = 5000,
|
|
}) {
|
|
const handle = await page.evaluateHandle(
|
|
(name, sel, validate, timeOut) => {
|
|
let callback = null,
|
|
timeoutId = null;
|
|
const element = sel ? document.querySelector(sel) : document;
|
|
return [
|
|
Promise.race([
|
|
new Promise(resolve => {
|
|
// The promise is resolved if the event fired in the context of the
|
|
// selector and, if a validator is defined, the event data satisfies
|
|
// the conditions of the validator function.
|
|
callback = e => {
|
|
if (timeoutId) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
// eslint-disable-next-line no-eval
|
|
resolve(validate ? eval(`(${validate})`)(e) : true);
|
|
};
|
|
element.addEventListener(name, callback, { once: true });
|
|
}),
|
|
new Promise(resolve => {
|
|
timeoutId = setTimeout(() => {
|
|
element.removeEventListener(name, callback);
|
|
resolve(null);
|
|
}, timeOut);
|
|
}),
|
|
]),
|
|
];
|
|
},
|
|
eventName,
|
|
selector,
|
|
validator ? validator.toString() : null,
|
|
timeout
|
|
);
|
|
|
|
await action();
|
|
|
|
const success = await awaitPromise(handle);
|
|
if (success === null) {
|
|
console.warn(
|
|
`waitForEvent: ${eventName} didn't trigger within the timeout`
|
|
);
|
|
} else if (!success) {
|
|
console.warn(`waitForEvent: ${eventName} triggered, but validation failed`);
|
|
}
|
|
}
|
|
|
|
async function countStorageEntries(page) {
|
|
return page.evaluate(
|
|
() => window.PDFViewerApplication.pdfDocument.annotationStorage.size
|
|
);
|
|
}
|
|
|
|
async function waitForStorageEntries(page, nEntries) {
|
|
return page.waitForFunction(
|
|
n => window.PDFViewerApplication.pdfDocument.annotationStorage.size === n,
|
|
{},
|
|
nEntries
|
|
);
|
|
}
|
|
|
|
async function countSerialized(page) {
|
|
return page.evaluate(
|
|
() =>
|
|
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map
|
|
?.size ?? 0
|
|
);
|
|
}
|
|
|
|
async function waitForSerialized(page, nEntries) {
|
|
return page.waitForFunction(
|
|
n => {
|
|
try {
|
|
return (
|
|
(window.PDFViewerApplication.pdfDocument.annotationStorage
|
|
.serializable.map?.size ?? 0) === n
|
|
);
|
|
} catch {
|
|
// When serializing a stamp annotation with a SVG, the transfer
|
|
// can fail because of the SVG, so we just retry.
|
|
return false;
|
|
}
|
|
},
|
|
{},
|
|
nEntries
|
|
);
|
|
}
|
|
|
|
async function applyFunctionToEditor(page, editorId, func) {
|
|
return page.evaluate(
|
|
(id, f) => {
|
|
const editor =
|
|
window.PDFViewerApplication.pdfDocument.annotationStorage.getRawValue(
|
|
id
|
|
);
|
|
// eslint-disable-next-line no-eval
|
|
eval(`(${f})`)(editor);
|
|
},
|
|
editorId,
|
|
func.toString()
|
|
);
|
|
}
|
|
|
|
async function selectEditor(page, selector, count = 1) {
|
|
const editorRect = await getRect(page, selector);
|
|
await page.mouse.click(
|
|
editorRect.x + editorRect.width / 2,
|
|
editorRect.y + editorRect.height / 2,
|
|
{ count }
|
|
);
|
|
await waitForSelectedEditor(page, selector);
|
|
}
|
|
|
|
async function waitForSelectedEditor(page, selector) {
|
|
return page.waitForSelector(`${selector}.selectedEditor`);
|
|
}
|
|
|
|
async function unselectEditor(page, selector) {
|
|
await page.keyboard.press("Escape");
|
|
await waitForUnselectedEditor(page, selector);
|
|
}
|
|
|
|
async function waitForUnselectedEditor(page, selector) {
|
|
return page.waitForSelector(`${selector}:not(.selectedEditor)`);
|
|
}
|
|
|
|
async function mockClipboard(pages) {
|
|
return Promise.all(
|
|
pages.map(async ([_, page]) => {
|
|
await page.evaluate(() => {
|
|
let data = null;
|
|
const clipboard = {
|
|
writeText: async text => (data = text),
|
|
readText: async () => data,
|
|
};
|
|
Object.defineProperty(navigator, "clipboard", { value: clipboard });
|
|
});
|
|
})
|
|
);
|
|
}
|
|
|
|
async function copy(page) {
|
|
await waitForEvent({
|
|
page,
|
|
eventName: "copy",
|
|
action: () => kbCopy(page),
|
|
});
|
|
}
|
|
|
|
async function copyToClipboard(page, data) {
|
|
await page.evaluate(async dat => {
|
|
const items = Object.create(null);
|
|
for (const [type, value] of Object.entries(dat)) {
|
|
if (value.startsWith("data:")) {
|
|
const resp = await fetch(value);
|
|
items[type] = await resp.blob();
|
|
} else {
|
|
items[type] = new Blob([value], { type });
|
|
}
|
|
}
|
|
await navigator.clipboard.write([new ClipboardItem(items)]);
|
|
}, data);
|
|
}
|
|
|
|
async function paste(page) {
|
|
await waitForEvent({
|
|
page,
|
|
eventName: "paste",
|
|
action: () => kbPaste(page),
|
|
});
|
|
}
|
|
|
|
async function pasteFromClipboard(page, selector = null) {
|
|
const validator = e => e.clipboardData.items.length !== 0;
|
|
await waitForEvent({
|
|
page,
|
|
eventName: "paste",
|
|
action: () => kbPaste(page),
|
|
selector,
|
|
validator,
|
|
});
|
|
}
|
|
|
|
async function getSerialized(page, filter = undefined) {
|
|
const values = await page.evaluate(() => {
|
|
const { map } =
|
|
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
|
|
if (!map) {
|
|
return [];
|
|
}
|
|
const vals = Array.from(map.values());
|
|
for (const value of vals) {
|
|
for (const [k, v] of Object.entries(value)) {
|
|
// Puppeteer don't serialize typed array correctly, so we convert them
|
|
// to arrays.
|
|
if (ArrayBuffer.isView(v)) {
|
|
value[k] = Array.from(v);
|
|
}
|
|
}
|
|
}
|
|
return vals;
|
|
});
|
|
return filter ? values.map(filter) : values;
|
|
}
|
|
|
|
async function getFirstSerialized(page, filter = undefined) {
|
|
return (await getSerialized(page, filter))[0];
|
|
}
|
|
|
|
function getAnnotationStorage(page) {
|
|
return page.evaluate(() =>
|
|
Object.fromEntries(
|
|
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable
|
|
.map || []
|
|
)
|
|
);
|
|
}
|
|
|
|
function waitForEntryInStorage(page, key, value, checker = (x, y) => x === y) {
|
|
return page.waitForFunction(
|
|
(k, v, c) => {
|
|
const { map } =
|
|
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
|
|
// eslint-disable-next-line no-eval
|
|
return map && eval(`(${c})`)(JSON.stringify(map.get(k)), v);
|
|
},
|
|
{},
|
|
key,
|
|
JSON.stringify(value),
|
|
checker.toString()
|
|
);
|
|
}
|
|
|
|
function getEditors(page, kind) {
|
|
return page.evaluate(aKind => {
|
|
const elements = document.querySelectorAll(`.${aKind}Editor`);
|
|
const results = [];
|
|
for (const { id } of elements) {
|
|
results.push(parseInt(id.split("_").at(-1), 10));
|
|
}
|
|
results.sort();
|
|
return results;
|
|
}, kind);
|
|
}
|
|
|
|
function getEditorDimensions(page, selector) {
|
|
return page.evaluate(sel => {
|
|
const { style } = document.querySelector(sel);
|
|
return {
|
|
left: style.left,
|
|
top: style.top,
|
|
width: style.width,
|
|
height: style.height,
|
|
};
|
|
}, selector);
|
|
}
|
|
|
|
async function serializeBitmapDimensions(page) {
|
|
await page.waitForFunction(() => {
|
|
try {
|
|
const map =
|
|
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable
|
|
.map;
|
|
return !!map;
|
|
} catch {
|
|
return false;
|
|
}
|
|
});
|
|
|
|
return page.evaluate(() => {
|
|
const { map } =
|
|
window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
|
|
return map
|
|
? Array.from(map.values(), x => ({
|
|
width: x.bitmap.width,
|
|
height: x.bitmap.height,
|
|
}))
|
|
: [];
|
|
});
|
|
}
|
|
|
|
async function dragAndDrop(page, selector, translations, steps = 1) {
|
|
const rect = await getRect(page, selector);
|
|
const startX = rect.x + rect.width / 2;
|
|
const startY = rect.y + rect.height / 2;
|
|
await page.mouse.move(startX, startY);
|
|
await page.mouse.down();
|
|
for (const [tX, tY] of translations) {
|
|
await page.mouse.move(startX + tX, startY + tY, { steps });
|
|
}
|
|
await page.mouse.up();
|
|
await page.waitForSelector("#viewer:not(.noUserSelect)");
|
|
}
|
|
|
|
function waitForPageChanging(page) {
|
|
return createPromise(page, resolve => {
|
|
window.PDFViewerApplication.eventBus.on("pagechanging", resolve, {
|
|
once: true,
|
|
});
|
|
});
|
|
}
|
|
|
|
function waitForAnnotationEditorLayer(page) {
|
|
return createPromise(page, resolve => {
|
|
window.PDFViewerApplication.eventBus.on(
|
|
"annotationeditorlayerrendered",
|
|
resolve,
|
|
{ once: true }
|
|
);
|
|
});
|
|
}
|
|
|
|
function waitForAnnotationModeChanged(page) {
|
|
return createPromise(page, resolve => {
|
|
window.PDFViewerApplication.eventBus.on(
|
|
"annotationeditormodechanged",
|
|
resolve,
|
|
{ once: true }
|
|
);
|
|
});
|
|
}
|
|
|
|
function waitForPageRendered(page, pageNumber) {
|
|
return page.evaluateHandle(
|
|
number => [
|
|
new Promise(resolve => {
|
|
const { eventBus } = window.PDFViewerApplication;
|
|
eventBus.on("pagerendered", function handler(e) {
|
|
if (
|
|
!e.isDetailView &&
|
|
(number === undefined || e.pageNumber === number)
|
|
) {
|
|
resolve();
|
|
eventBus.off("pagerendered", handler);
|
|
}
|
|
});
|
|
}),
|
|
],
|
|
pageNumber
|
|
);
|
|
}
|
|
|
|
function waitForEditorMovedInDOM(page) {
|
|
return createPromise(page, resolve => {
|
|
window.PDFViewerApplication.eventBus.on("editormovedindom", resolve, {
|
|
once: true,
|
|
});
|
|
});
|
|
}
|
|
|
|
async function scrollIntoView(page, selector) {
|
|
await page.waitForSelector(selector, { visible: true });
|
|
const handle = await page.evaluateHandle(
|
|
sel => [
|
|
new Promise(resolve => {
|
|
const container = document.getElementById("viewerContainer");
|
|
const element = document.querySelector(sel);
|
|
if (!container || !element) {
|
|
resolve();
|
|
return;
|
|
}
|
|
if (
|
|
container.scrollHeight <= container.clientHeight &&
|
|
container.scrollWidth <= container.clientWidth
|
|
) {
|
|
resolve();
|
|
return;
|
|
}
|
|
|
|
const beforeTop = container.scrollTop;
|
|
const beforeLeft = container.scrollLeft;
|
|
let settled = false;
|
|
let timeoutId = null;
|
|
|
|
const finish = () => {
|
|
if (settled) {
|
|
return;
|
|
}
|
|
settled = true;
|
|
if (timeoutId !== null) {
|
|
clearTimeout(timeoutId);
|
|
}
|
|
container.removeEventListener("scrollend", finish);
|
|
resolve();
|
|
};
|
|
|
|
container.addEventListener("scrollend", finish, { once: true });
|
|
element.scrollIntoView({ behavior: "instant", block: "start" });
|
|
|
|
if (
|
|
container.scrollTop === beforeTop &&
|
|
container.scrollLeft === beforeLeft
|
|
) {
|
|
finish();
|
|
return;
|
|
}
|
|
|
|
// Some browsers occasionally miss `scrollend`, so keep a short
|
|
// fallback to avoid hanging.
|
|
timeoutId = setTimeout(finish, 250);
|
|
}),
|
|
],
|
|
selector
|
|
);
|
|
return awaitPromise(handle);
|
|
}
|
|
|
|
async function firstPageOnTop(page) {
|
|
const handle = await page.evaluateHandle(() => [
|
|
new Promise(resolve => {
|
|
const container = document.getElementById("viewerContainer");
|
|
if (container.scrollTop === 0 && container.scrollLeft === 0) {
|
|
resolve();
|
|
return;
|
|
}
|
|
container.addEventListener("scrollend", resolve, { once: true });
|
|
container.scrollTo(0, 0);
|
|
}),
|
|
]);
|
|
return awaitPromise(handle);
|
|
}
|
|
|
|
async function setCaretAt(page, pageNumber, text, position) {
|
|
// Wait for the text layer to finish rendering before trying to find the span.
|
|
await page.waitForSelector(
|
|
`.page[data-page-number="${pageNumber}"] .textLayer .endOfContent`
|
|
);
|
|
await page.evaluate(
|
|
(pageN, string, pos) => {
|
|
for (const el of document.querySelectorAll(
|
|
`.page[data-page-number="${pageN}"] > .textLayer > span`
|
|
)) {
|
|
if (el.textContent === string) {
|
|
window.getSelection().setPosition(el.firstChild, pos);
|
|
break;
|
|
}
|
|
}
|
|
},
|
|
pageNumber,
|
|
text,
|
|
position
|
|
);
|
|
}
|
|
|
|
const modifier = isMac ? "Meta" : "Control";
|
|
async function kbCopy(page) {
|
|
await page.keyboard.down(modifier);
|
|
await page.keyboard.press("c", { commands: ["Copy"] });
|
|
await page.keyboard.up(modifier);
|
|
}
|
|
async function kbCut(page) {
|
|
await page.keyboard.down(modifier);
|
|
await page.keyboard.press("x", { commands: ["Cut"] });
|
|
await page.keyboard.up(modifier);
|
|
}
|
|
async function kbDelete(page) {
|
|
await page.keyboard.press("Delete");
|
|
}
|
|
async function kbPaste(page) {
|
|
await page.keyboard.down(modifier);
|
|
await page.keyboard.press("v", { commands: ["Paste"] });
|
|
await page.keyboard.up(modifier);
|
|
}
|
|
async function kbUndo(page) {
|
|
await page.keyboard.down(modifier);
|
|
await page.keyboard.press("z");
|
|
await page.keyboard.up(modifier);
|
|
}
|
|
async function kbRedo(page) {
|
|
if (isMac) {
|
|
await page.keyboard.down("Meta");
|
|
await page.keyboard.down("Shift");
|
|
await page.keyboard.press("z");
|
|
await page.keyboard.up("Shift");
|
|
await page.keyboard.up("Meta");
|
|
} else {
|
|
await page.keyboard.down("Control");
|
|
await page.keyboard.press("y");
|
|
await page.keyboard.up("Control");
|
|
}
|
|
}
|
|
async function kbSelectAll(page) {
|
|
await page.keyboard.down(modifier);
|
|
await page.keyboard.press("a", { commands: ["SelectAll"] });
|
|
await page.keyboard.up(modifier);
|
|
}
|
|
async function kbModifierDown(page) {
|
|
await page.keyboard.down(modifier);
|
|
}
|
|
async function kbModifierUp(page) {
|
|
await page.keyboard.up(modifier);
|
|
}
|
|
async function kbGoToEnd(page) {
|
|
if (isMac) {
|
|
await page.keyboard.down("Meta");
|
|
await page.keyboard.press("ArrowDown", {
|
|
commands: ["MoveToEndOfDocument"],
|
|
});
|
|
await page.keyboard.up("Meta");
|
|
} else {
|
|
await page.keyboard.down("Control");
|
|
await page.keyboard.press("End");
|
|
await page.keyboard.up("Control");
|
|
}
|
|
}
|
|
async function kbGoToBegin(page) {
|
|
if (isMac) {
|
|
await page.keyboard.down("Meta");
|
|
await page.keyboard.press("ArrowUp", {
|
|
commands: ["MoveToBeginningOfDocument"],
|
|
});
|
|
await page.keyboard.up("Meta");
|
|
} else {
|
|
await page.keyboard.down("Control");
|
|
await page.keyboard.press("Home");
|
|
await page.keyboard.up("Control");
|
|
}
|
|
}
|
|
async function kbBigMoveLeft(page) {
|
|
if (isMac) {
|
|
await page.keyboard.down("Shift");
|
|
await page.keyboard.press("ArrowLeft");
|
|
await page.keyboard.up("Shift");
|
|
} else {
|
|
await page.keyboard.down("Control");
|
|
await page.keyboard.press("ArrowLeft");
|
|
await page.keyboard.up("Control");
|
|
}
|
|
}
|
|
async function kbBigMoveRight(page) {
|
|
if (isMac) {
|
|
await page.keyboard.down("Shift");
|
|
await page.keyboard.press("ArrowRight");
|
|
await page.keyboard.up("Shift");
|
|
} else {
|
|
await page.keyboard.down("Control");
|
|
await page.keyboard.press("ArrowRight");
|
|
await page.keyboard.up("Control");
|
|
}
|
|
}
|
|
async function kbBigMoveUp(page) {
|
|
if (isMac) {
|
|
await page.keyboard.down("Shift");
|
|
await page.keyboard.press("ArrowUp");
|
|
await page.keyboard.up("Shift");
|
|
} else {
|
|
await page.keyboard.down("Control");
|
|
await page.keyboard.press("ArrowUp");
|
|
await page.keyboard.up("Control");
|
|
}
|
|
}
|
|
async function kbBigMoveDown(page) {
|
|
if (isMac) {
|
|
await page.keyboard.down("Shift");
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.up("Shift");
|
|
} else {
|
|
await page.keyboard.down("Control");
|
|
await page.keyboard.press("ArrowDown");
|
|
await page.keyboard.up("Control");
|
|
}
|
|
}
|
|
|
|
async function kbDeleteLastWord(page) {
|
|
if (isMac) {
|
|
await page.keyboard.down("Alt");
|
|
await page.keyboard.press("Backspace");
|
|
await page.keyboard.up("Alt");
|
|
} else {
|
|
await page.keyboard.down("Control");
|
|
await page.keyboard.press("Backspace");
|
|
await page.keyboard.up("Control");
|
|
}
|
|
}
|
|
|
|
async function kbFocusNext(page, selector = null) {
|
|
if (selector) {
|
|
await page.waitForSelector(selector, { visible: true });
|
|
}
|
|
while (true) {
|
|
const handle = await page.evaluateHandle(
|
|
sel => [
|
|
new Promise(resolve => {
|
|
const cb = e => {
|
|
if (!sel || document.querySelector(sel)?.contains(e.target)) {
|
|
window.removeEventListener("focusin", cb);
|
|
resolve(true);
|
|
} else {
|
|
resolve(false);
|
|
}
|
|
};
|
|
window.addEventListener("focusin", cb);
|
|
}),
|
|
],
|
|
selector
|
|
);
|
|
|
|
await page.keyboard.press("Tab");
|
|
const result = await awaitPromise(handle);
|
|
if (result) {
|
|
break;
|
|
}
|
|
}
|
|
if (selector) {
|
|
await page.waitForSelector(`${selector}:focus`, { visible: true });
|
|
}
|
|
}
|
|
|
|
async function kbFocusPrevious(page) {
|
|
const handle = await createPromise(page, resolve => {
|
|
window.addEventListener("focusin", resolve, { once: true });
|
|
});
|
|
await page.keyboard.down("Shift");
|
|
await page.keyboard.press("Tab");
|
|
await page.keyboard.up("Shift");
|
|
await awaitPromise(handle);
|
|
}
|
|
|
|
async function kbSave(page) {
|
|
await page.keyboard.down(modifier);
|
|
await page.keyboard.press("s");
|
|
await page.keyboard.up(modifier);
|
|
}
|
|
|
|
async function switchToEditor(name, page, disable = false) {
|
|
const modeChangedHandle = await createPromise(page, resolve => {
|
|
window.PDFViewerApplication.eventBus.on(
|
|
"annotationeditormodechanged",
|
|
resolve,
|
|
{ once: true }
|
|
);
|
|
});
|
|
await page.click(`#editor${name}Button`);
|
|
name = name.toLowerCase();
|
|
await page.waitForSelector(
|
|
".annotationEditorLayer" +
|
|
(disable ? `:not(.${name}Editing)` : `.${name}Editing`)
|
|
);
|
|
await awaitPromise(modeChangedHandle);
|
|
}
|
|
|
|
async function selectEditors(name, page) {
|
|
await kbSelectAll(page);
|
|
await page.waitForFunction(
|
|
() => !document.querySelector(`.${name}Editor:not(.selectedEditor)`)
|
|
);
|
|
}
|
|
|
|
async function clearEditors(name, page) {
|
|
await selectEditors(name, page);
|
|
await page.keyboard.press("Backspace");
|
|
await waitForStorageEntries(page, 0);
|
|
}
|
|
|
|
function waitForNoElement(page, selector) {
|
|
return page.waitForFunction(
|
|
sel => !document.querySelector(sel),
|
|
{},
|
|
selector
|
|
);
|
|
}
|
|
|
|
function waitForTextToBe(page, selector, text) {
|
|
return page.waitForFunction(
|
|
(sel, str) => document.querySelector(sel)?.textContent.trim() === str,
|
|
{},
|
|
selector,
|
|
text
|
|
);
|
|
}
|
|
|
|
function waitForTooltipToBe(page, selector, text) {
|
|
return page.waitForFunction(
|
|
(sel, str) => document.querySelector(sel)?.title === str,
|
|
{},
|
|
selector,
|
|
text
|
|
);
|
|
}
|
|
|
|
function isCanvasMonochrome(page, pageNumber, rectangle, color) {
|
|
return page.evaluate(
|
|
(rect, pageN, col) => {
|
|
const canvas = document.querySelector(
|
|
`.page[data-page-number = "${pageN}"] .canvasWrapper canvas`
|
|
);
|
|
const canvasRect = canvas.getBoundingClientRect();
|
|
const ctx = canvas.getContext("2d");
|
|
rect ||= canvasRect;
|
|
const { data } = ctx.getImageData(
|
|
rect.x - canvasRect.x,
|
|
rect.y - canvasRect.y,
|
|
rect.width,
|
|
rect.height
|
|
);
|
|
return new Uint32Array(data.buffer).every(x => x === col);
|
|
},
|
|
rectangle,
|
|
pageNumber,
|
|
color
|
|
);
|
|
}
|
|
|
|
async function getXY(page, selector) {
|
|
const rect = await getRect(page, selector);
|
|
return `${rect.x}::${rect.y}`;
|
|
}
|
|
|
|
function waitForPositionChange(page, selector, xy) {
|
|
return page.waitForFunction(
|
|
(sel, currentXY) => {
|
|
const bbox = document.querySelector(sel).getBoundingClientRect();
|
|
return `${bbox.x}::${bbox.y}` !== currentXY;
|
|
},
|
|
{},
|
|
selector,
|
|
xy
|
|
);
|
|
}
|
|
|
|
async function moveEditor(page, selector, n, pressKey) {
|
|
let xy = await getXY(page, selector);
|
|
for (let i = 0; i < n; i++) {
|
|
const handle = await waitForEditorMovedInDOM(page);
|
|
await pressKey();
|
|
await awaitPromise(handle);
|
|
await waitForPositionChange(page, selector, xy);
|
|
xy = await getXY(page, selector);
|
|
}
|
|
}
|
|
|
|
async function getNextEditorId(page) {
|
|
return page.evaluate(() =>
|
|
window.PDFViewerApplication.pdfViewer._layerProperties.annotationEditorUIManager.getNextEditorId()
|
|
);
|
|
}
|
|
|
|
async function highlightSpan(
|
|
page,
|
|
pageIndex,
|
|
text,
|
|
xRatio = 0.5,
|
|
yRatio = 0.5
|
|
) {
|
|
const nextId = await getNextEditorId(page);
|
|
const rect = await getSpanRectFromText(page, pageIndex, text);
|
|
const x = rect.x + rect.width * xRatio;
|
|
const y = rect.y + rect.height * yRatio;
|
|
// We add a small delay between press and release to make sure that a
|
|
// pointerup event is triggered after selectionchange.
|
|
// It works with a value of 1ms, but we use 100ms to be sure.
|
|
await page.mouse.click(x, y, { count: 2, delay: 100 });
|
|
await page.waitForSelector(getEditorSelector(nextId));
|
|
}
|
|
|
|
async function showViewsManager(page) {
|
|
const hasAnimations = await page.evaluate(
|
|
() => !window.matchMedia("(prefers-reduced-motion: reduce)").matches
|
|
);
|
|
const movingPromise = hasAnimations
|
|
? page.waitForSelector("#outerContainer.viewsManagerMoving", {
|
|
visible: true,
|
|
})
|
|
: Promise.resolve();
|
|
await page.click("#viewsManagerToggleButton");
|
|
if (hasAnimations) {
|
|
await movingPromise;
|
|
}
|
|
await page.waitForSelector("#viewsManager", { visible: true });
|
|
await page.waitForSelector(
|
|
"#outerContainer:not(.viewsManagerMoving).viewsManagerOpen",
|
|
{ visible: true }
|
|
);
|
|
await page.waitForSelector("#viewsManagerStatusActionButton:not(:disabled)", {
|
|
visible: true,
|
|
});
|
|
}
|
|
|
|
async function waitForBrowserTrip(page) {
|
|
const handle = await page.evaluateHandle(() => [
|
|
new Promise(resolve => {
|
|
window.requestAnimationFrame(resolve);
|
|
}),
|
|
]);
|
|
await awaitPromise(handle);
|
|
}
|
|
|
|
// Unicode bidi isolation characters, Fluent adds these markers to the text.
|
|
const FSI = "\u2068";
|
|
const PDI = "\u2069";
|
|
|
|
export {
|
|
applyFunctionToEditor,
|
|
awaitPromise,
|
|
clearEditors,
|
|
clearInput,
|
|
closePages,
|
|
closeSinglePage,
|
|
copy,
|
|
copyToClipboard,
|
|
countSerialized,
|
|
countStorageEntries,
|
|
createPromise,
|
|
createPromiseWithArgs,
|
|
dragAndDrop,
|
|
firstPageOnTop,
|
|
FSI,
|
|
getAnnotationSelector,
|
|
getAnnotationStorage,
|
|
getComputedStyleSelector,
|
|
getEditorDimensions,
|
|
getEditors,
|
|
getEditorSelector,
|
|
getFirstSerialized,
|
|
getNextEditorId,
|
|
getQuerySelector,
|
|
getRect,
|
|
getSelector,
|
|
getSerialized,
|
|
getSpanRectFromText,
|
|
getThumbnailSelector,
|
|
getXY,
|
|
highlightSpan,
|
|
isCanvasMonochrome,
|
|
kbBigMoveDown,
|
|
kbBigMoveLeft,
|
|
kbBigMoveRight,
|
|
kbBigMoveUp,
|
|
kbCopy,
|
|
kbCut,
|
|
kbDelete,
|
|
kbDeleteLastWord,
|
|
kbFocusNext,
|
|
kbFocusPrevious,
|
|
kbGoToBegin,
|
|
kbGoToEnd,
|
|
kbModifierDown,
|
|
kbModifierUp,
|
|
kbRedo,
|
|
kbSave,
|
|
kbSelectAll,
|
|
kbUndo,
|
|
loadAndWait,
|
|
mockClipboard,
|
|
moveEditor,
|
|
paste,
|
|
pasteFromClipboard,
|
|
PDI,
|
|
scrollIntoView,
|
|
selectEditor,
|
|
selectEditors,
|
|
serializeBitmapDimensions,
|
|
setCaretAt,
|
|
showViewsManager,
|
|
switchToEditor,
|
|
unselectEditor,
|
|
waitAndClick,
|
|
waitForAnnotationEditorLayer,
|
|
waitForAnnotationModeChanged,
|
|
waitForBrowserTrip,
|
|
waitForDOMMutation,
|
|
waitForEntryInStorage,
|
|
waitForEvent,
|
|
waitForNoElement,
|
|
waitForPageChanging,
|
|
waitForPageRendered,
|
|
waitForPointerUp,
|
|
waitForSandboxTrip,
|
|
waitForSelectedEditor,
|
|
waitForSerialized,
|
|
waitForStorageEntries,
|
|
waitForTextToBe,
|
|
waitForTimeout,
|
|
waitForTooltipToBe,
|
|
waitForUnselectedEditor,
|
|
};
|