Files
PDF.js/test/integration/viewer_spec.mjs
Yu Cong 44db9807a1
Some checks failed
Types tests / Test (lts/*) (push) Has been cancelled
Lint / Lint (lts/*) (push) Has been cancelled
CodeQL / Analyze (javascript) (push) Has been cancelled
CI / Test (20) (push) Has been cancelled
CI / Test (22) (push) Has been cancelled
CI / Test (24) (push) Has been cancelled
first commit
2025-10-03 22:20:19 +08:00

1465 lines
49 KiB
JavaScript

/* Copyright 2024 Mozilla Foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
awaitPromise,
closePages,
createPromise,
getRect,
getSpanRectFromText,
loadAndWait,
scrollIntoView,
waitForPageChanging,
waitForPageRendered,
} from "./test_utils.mjs";
import { PNG } from "pngjs";
describe("PDF viewer", () => {
describe("Zoom origin", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
"page-width",
null,
{ page: 2 }
);
});
afterEach(async () => {
await closePages(pages);
});
async function waitForTextAfterZoom(page, originX, originY, scale, text) {
const handlePromise = await createPromise(page, resolve => {
const callback = e => {
if (e.pageNumber === 2) {
window.PDFViewerApplication.eventBus.off(
"textlayerrendered",
callback
);
resolve();
}
};
window.PDFViewerApplication.eventBus.on("textlayerrendered", callback);
});
await page.evaluate(
(scaleFactor, origin) => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: 0,
scaleFactor,
origin,
});
},
scale,
[originX, originY]
);
await awaitPromise(handlePromise);
await page.waitForFunction(
`document.elementFromPoint(${originX}, ${originY})?.textContent === "${text}"`
);
}
it("supports specifying a custom origin", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// We use this text span of page 2 because:
// - it's in the visible area even when zooming at page-width
// - it's small, so it easily catches if the page moves too much
// - it's in a "random" position: not near the center of the
// viewport, and not near the borders
const text = "guards";
const rect = await getSpanRectFromText(page, 2, text);
const originX = rect.x + rect.width / 2;
const originY = rect.y + rect.height / 2;
await waitForTextAfterZoom(page, originX, originY, 2, text);
await waitForTextAfterZoom(page, originX, originY, 0.8, text);
})
);
});
});
describe("Zoom with the mouse wheel", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty.pdf", ".textLayer .endOfContent", 1000);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that we can zoom with the mouse wheel and pressed control key", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.keyboard.down("Control");
let zoom = 10;
const zoomGetter = () =>
page.evaluate(
() => window.PDFViewerApplication.pdfViewer.currentScale
);
while (zoom > 0.1) {
await page.mouse.wheel({ deltaY: 100 });
zoom = await zoomGetter();
}
while (zoom < 10) {
await page.mouse.wheel({ deltaY: -100 });
zoom = await zoomGetter();
}
await page.keyboard.up("Control");
})
);
});
});
describe("Zoom commands", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that zoom commands don't scroll the document", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
for (let i = 0; i < 10; i++) {
await page.evaluate(() => window.PDFViewerApplication.zoomIn());
await page.evaluate(() => window.PDFViewerApplication.zoomReset());
await page.waitForSelector(
`.page[data-page-number="1"] .textLayer .endOfContent`
);
const scrollTop = await page.evaluate(
() => document.getElementById("viewerContainer").scrollTop
);
expect(scrollTop < 100)
.withContext(`In ${browserName}`)
.toBe(true);
}
})
);
});
});
describe("CSS-only zoom", () => {
function createPromiseForFirstPageRendered(page) {
return createPromise(page, (resolve, reject) => {
const controller = new AbortController();
window.PDFViewerApplication.eventBus.on(
"pagerendered",
({ pageNumber, timestamp }) => {
if (pageNumber === 1) {
resolve(timestamp);
controller.abort();
}
},
{ signal: controller.signal }
);
setTimeout(reject, 1000, new Error("Timeout"));
});
}
describe("forced (maxCanvasPixels: 0)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
null,
null,
{ maxCanvasPixels: 0 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("respects drawing delay when zooming out", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const promise = await createPromiseForFirstPageRendered(page);
const start = await page.evaluate(() => {
const startTime = performance.now();
window.PDFViewerApplication.pdfViewer.decreaseScale({
drawingDelay: 100,
scaleFactor: 0.9,
});
return startTime;
});
const end = await awaitPromise(promise);
expect(end - start)
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(100);
})
);
});
it("respects drawing delay when zooming in", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const promise = await createPromiseForFirstPageRendered(page);
const start = await page.evaluate(() => {
const startTime = performance.now();
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 100,
scaleFactor: 1.1,
});
return startTime;
});
const end = await awaitPromise(promise);
expect(end - start)
.withContext(`In ${browserName}`)
.toBeGreaterThanOrEqual(100);
})
);
});
});
describe("triggers when going bigger than maxCanvasPixels", () => {
let pages;
const MAX_CANVAS_PIXELS = new Map();
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
".textLayer .endOfContent",
null,
null,
async (page, browserName) => {
const ratio = await page.evaluate(() => window.devicePixelRatio);
const maxCanvasPixels = 1_000_000 * ratio ** 2;
MAX_CANVAS_PIXELS.set(browserName, maxCanvasPixels);
return { maxCanvasPixels };
}
);
await Promise.all(
pages.map(async ([browserName, page]) => {
const handle = await waitForPageRendered(page);
if (
await page.evaluate(() => {
if (
window.PDFViewerApplication.pdfViewer.currentScale !== 0.5
) {
window.PDFViewerApplication.pdfViewer.currentScale = 0.5;
return true;
}
return false;
})
) {
await awaitPromise(handle);
}
})
);
});
afterEach(async () => {
await closePages(pages);
});
function getCanvasSize(page) {
return page.evaluate(() => {
const canvas = window.document.querySelector(".canvasWrapper canvas");
return canvas.width * canvas.height;
});
}
// MAX_CANVAS_PIXELS must be big enough that the originally rendered
// canvas still has enough space to grow before triggering CSS-only zoom
it("test correctly configured", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const canvasSize = await getCanvasSize(page);
expect(canvasSize)
.withContext(`In ${browserName}`)
.toBeLessThan(MAX_CANVAS_PIXELS.get(browserName) / 4);
expect(canvasSize)
.withContext(`In ${browserName}`)
.toBeGreaterThan(MAX_CANVAS_PIXELS.get(browserName) / 16);
})
);
});
it("does not trigger CSS-only zoom below maxCanvasPixels", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const originalCanvasSize = await getCanvasSize(page);
const factor = 2;
const handle = await waitForPageRendered(page, 1);
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
await awaitPromise(handle);
const canvasSize = await getCanvasSize(page);
expect(canvasSize)
.withContext(`In ${browserName}`)
.toBe(originalCanvasSize * factor ** 2);
expect(canvasSize)
.withContext(`In ${browserName}, MAX_CANVAS_PIXELS`)
.toBeLessThan(MAX_CANVAS_PIXELS.get(browserName));
})
);
});
it("triggers CSS-only zoom above maxCanvasPixels", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const originalCanvasSize = await getCanvasSize(page);
const factor = 4;
const handle = await waitForPageRendered(page, 1);
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.increaseScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
await awaitPromise(handle);
const canvasSize = await getCanvasSize(page);
expect(canvasSize)
.withContext(`In ${browserName}`)
.toBeLessThan(originalCanvasSize * factor ** 2);
expect(canvasSize)
.withContext(`In ${browserName}, <= MAX_CANVAS_PIXELS / 4`)
.toBeLessThanOrEqual(MAX_CANVAS_PIXELS.get(browserName) / 4);
expect(canvasSize)
.withContext(`In ${browserName}, > MAX_CANVAS_PIXELS / 4 * 0.95`)
.toBeGreaterThan((MAX_CANVAS_PIXELS.get(browserName) / 4) * 0.95);
})
);
});
});
});
describe("Canvas fits the page", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"issue18694.pdf",
".textLayer .endOfContent",
"page-width",
null,
{ capCanvasAreaFactor: -1 }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that canvas perfectly fits the page whatever the zoom level is", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// The pdf has a single page with a red background.
// We set the viewer background to red, because when screenshoting
// some part of the viewer background can be visible.
// But here we don't care about the viewer background: we only
// care about the page background and the canvas default color.
await page.evaluate(() => {
document.body.style.background = "#ff0000";
});
for (let i = 0; ; i++) {
const handle = await waitForPageRendered(page);
await page.evaluate(() => window.PDFViewerApplication.zoomOut());
await awaitPromise(handle);
await scrollIntoView(page, `.page[data-page-number="1"]`);
const element = await page.$(`.page[data-page-number="1"]`);
const png = await element.screenshot({ type: "png" });
const pageImage = PNG.sync.read(Buffer.from(png));
let buffer = new Uint32Array(pageImage.data.buffer);
// Search for the first red pixel.
const j = buffer.indexOf(0xff0000ff);
buffer = buffer.slice(j);
expect(buffer.every(x => x === 0xff0000ff))
.withContext(`In ${browserName}, in the ${i}th zoom in`)
.toBe(true);
const currentScale = await page.evaluate(
() => window.PDFViewerApplication.pdfViewer.currentScale
);
if (currentScale <= 0.1) {
break;
}
}
})
);
});
});
describe("Detail view on zoom", () => {
const BASE_MAX_CANVAS_PIXELS = 1e6;
function setupPages(
zoom,
devicePixelRatio,
capCanvasAreaFactor,
setups = {}
) {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"colors.pdf",
null,
zoom,
{
// When running Firefox with Puppeteer, setting the
// devicePixelRatio Puppeteer option does not properly set
// the `window.devicePixelRatio` value. Set it manually.
earlySetup: `() => {
window.devicePixelRatio = ${devicePixelRatio};
}`,
...setups,
},
{
maxCanvasPixels: BASE_MAX_CANVAS_PIXELS * devicePixelRatio ** 2,
capCanvasAreaFactor,
},
{ height: 600, width: 800, devicePixelRatio }
);
});
afterEach(async () => {
await closePages(pages);
});
return function forEachPage(fn) {
return Promise.all(
pages.map(([browserName, page]) => fn(browserName, page))
);
};
}
function extractCanvases(pageNumber) {
const pageOne = document.querySelector(
`.page[data-page-number='${pageNumber}']`
);
return Array.from(pageOne.querySelectorAll("canvas"), canvas => {
const { width, height } = canvas;
const ctx = canvas.getContext("2d");
const topLeft = ctx.getImageData(2, 2, 1, 1).data;
const bottomRight = ctx.getImageData(width - 3, height - 3, 1, 1).data;
return {
size: width * height,
width,
height,
topLeft: globalThis.pdfjsLib.Util.makeHexColor(...topLeft),
bottomRight: globalThis.pdfjsLib.Util.makeHexColor(...bottomRight),
};
});
}
function waitForDetailRendered(page) {
return createPromise(page, resolve => {
const controller = new AbortController();
window.PDFViewerApplication.eventBus.on(
"pagerendered",
({ isDetailView }) => {
if (isDetailView) {
resolve();
controller.abort();
}
},
{ signal: controller.signal }
);
});
}
for (const pixelRatio of [1, 2]) {
describe(`with pixel ratio ${pixelRatio}`, () => {
describe("setupPages()", () => {
const forEachPage = setupPages("100%", pixelRatio, -1);
it("sets the proper devicePixelRatio", async () => {
await forEachPage(async (browserName, page) => {
const devicePixelRatio = await page.evaluate(
() => window.devicePixelRatio
);
expect(devicePixelRatio)
.withContext(`In ${browserName}`)
.toBe(pixelRatio);
});
});
});
describe("when zooming with a cap on the canvas dimensions", () => {
const forEachPage = setupPages("10%", pixelRatio, 0);
it("must render the detail view", async () => {
await forEachPage(async (browserName, page) => {
await page.waitForSelector(
".page[data-page-number='1'] .textLayer"
);
const before = await page.evaluate(extractCanvases, 1);
expect(before.length)
.withContext(`In ${browserName}, before`)
.toBe(1);
const factor = 50;
const handle = await waitForDetailRendered(page);
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
await awaitPromise(handle);
const after = await page.evaluate(extractCanvases, 1);
// The page dimensions are 595x841, so the base canvas is a scale
// version of that but the number of pixels is capped to
// 800x600 = 480000.
expect(after.length)
.withContext(`In ${browserName}, after`)
.toBe(2);
expect(after[0].width)
.withContext(`In ${browserName}`)
.toBe(Math.floor(291 * pixelRatio));
expect(after[0].height)
.withContext(`In ${browserName}`)
.toBe(Math.floor(411.5 * pixelRatio));
// The dimensions of the detail canvas are capped to 800x600 but
// it depends on the visible area which depends itself of the
// scrollbars dimensions, hence we just check that the canvas
// dimensions are capped.
expect(after[1].width)
.withContext(`In ${browserName}`)
.toBeLessThan(810 * pixelRatio);
expect(after[1].height)
.withContext(`In ${browserName}`)
.toBeLessThan(575 * pixelRatio);
expect(after[1].size)
.withContext(`In ${browserName}`)
.toBeLessThan(800 * 600 * pixelRatio ** 2);
});
});
});
describe("when zooming in past max canvas size", () => {
const forEachPage = setupPages("100%", pixelRatio, -1);
it("must render the detail view", async () => {
await forEachPage(async (browserName, page) => {
await page.waitForSelector(
".page[data-page-number='1'] .textLayer"
);
const before = await page.evaluate(extractCanvases, 1);
expect(before.length)
.withContext(`In ${browserName}, before`)
.toBe(1);
expect(before[0].size)
.withContext(`In ${browserName}, before`)
.toBeLessThan(BASE_MAX_CANVAS_PIXELS * pixelRatio ** 2);
expect(before[0])
.withContext(`In ${browserName}, before`)
.toEqual(
jasmine.objectContaining({
topLeft: "#85200c", // dark berry
bottomRight: "#b6d7a8", // light green
})
);
const factor = 3;
// Check that we are going to trigger CSS zoom.
expect(before[0].size * factor ** 2)
.withContext(`In ${browserName}`)
.toBeGreaterThan(BASE_MAX_CANVAS_PIXELS * pixelRatio ** 2);
const handle = await waitForDetailRendered(page);
await page.evaluate(scaleFactor => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: 0,
scaleFactor,
});
}, factor);
await awaitPromise(handle);
const after = await page.evaluate(extractCanvases, 1);
expect(after.length)
.withContext(`In ${browserName}, after`)
.toBe(2);
expect(after[0].size)
.withContext(`In ${browserName}, after (first)`)
.toBeLessThan(4e6);
expect(after[0])
.withContext(`In ${browserName}, after (first)`)
.toEqual(
jasmine.objectContaining({
topLeft: "#85200c", // dark berry
bottomRight: "#b6d7a8", // light green
})
);
expect(after[1].size)
.withContext(`In ${browserName}, after (second)`)
.toBeLessThan(4e6);
expect(after[1])
.withContext(`In ${browserName}, after (second)`)
.toEqual(
jasmine.objectContaining({
topLeft: "#85200c", // dark berry
bottomRight: "#ff0000", // bright red
})
);
});
});
});
describe("when starting already zoomed in past max canvas size", () => {
const forEachPage = setupPages("300%", pixelRatio, -1);
it("must render the detail view", async () => {
await forEachPage(async (browserName, page) => {
await page.waitForSelector(
".page[data-page-number='1'] canvas:nth-child(2)"
);
const canvases = await page.evaluate(extractCanvases, 1);
expect(canvases.length).withContext(`In ${browserName}`).toBe(2);
expect(canvases[0].size)
.withContext(`In ${browserName} (first)`)
.toBeLessThan(4e6);
expect(canvases[0])
.withContext(`In ${browserName} (first)`)
.toEqual(
jasmine.objectContaining({
topLeft: "#85200c", // dark berry
bottomRight: "#b6d7a8", // light green
})
);
expect(canvases[1].size)
.withContext(`In ${browserName} (second)`)
.toBeLessThan(4e6);
expect(canvases[1])
.withContext(`In ${browserName} (second)`)
.toEqual(
jasmine.objectContaining({
topLeft: "#85200c", // dark berry
bottomRight: "#ff0000", // bright red
})
);
});
});
});
describe("when scrolling", () => {
const forEachPage = setupPages("300%", pixelRatio, -1);
it("must update the detail view", async () => {
await forEachPage(async (browserName, page) => {
await page.waitForSelector(
".page[data-page-number='1'] canvas:nth-child(2)"
);
const handle = await waitForDetailRendered(page);
await page.evaluate(() => {
const container = document.getElementById("viewerContainer");
container.scrollTop += 1600;
container.scrollLeft += 1100;
});
await awaitPromise(handle);
const canvases = await page.evaluate(extractCanvases, 1);
expect(canvases.length).withContext(`In ${browserName}`).toBe(2);
expect(canvases[1].size)
.withContext(`In ${browserName}`)
.toBeLessThan(4e6);
expect(canvases[1])
.withContext(`In ${browserName}`)
.toEqual(
jasmine.objectContaining({
topLeft: "#ff9900", // bright orange
bottomRight: "#ffe599", // light yellow
})
);
});
});
});
describe("when scrolling little enough that the existing detail covers the new viewport", () => {
const forEachPage = setupPages("300%", pixelRatio, -1);
it("must not re-create the detail canvas", async () => {
await forEachPage(async (browserName, page) => {
const detailCanvasSelector =
".page[data-page-number='1'] canvas:nth-child(2)";
await page.waitForSelector(detailCanvasSelector);
const detailCanvasHandle = await page.$(detailCanvasSelector);
let rendered = false;
const handle = await waitForDetailRendered(page);
await page.evaluate(() => {
const container = document.getElementById("viewerContainer");
container.scrollTop += 10;
container.scrollLeft += 10;
});
awaitPromise(handle)
.then(() => {
rendered = true;
})
.catch(() => {});
// Give some time to the page to re-render. If it re-renders it's
// a bug, but without waiting we would never catch it.
await new Promise(resolve => {
setTimeout(resolve, 100);
});
const isSame = await page.evaluate(
(prev, selector) => prev === document.querySelector(selector),
detailCanvasHandle,
detailCanvasSelector
);
expect(isSame).withContext(`In ${browserName}`).toBe(true);
expect(rendered).withContext(`In ${browserName}`).toBe(false);
});
});
});
describe("when scrolling to have two visible pages", () => {
const forEachPage = setupPages("300%", pixelRatio, -1);
it("must update the detail view", async () => {
await forEachPage(async (browserName, page) => {
await page.waitForSelector(
".page[data-page-number='1'] canvas:nth-child(2)"
);
const handle = await createPromise(page, resolve => {
// wait for two 'pagerendered' events for detail views
let second = false;
const { eventBus } = window.PDFViewerApplication;
eventBus.on(
"pagerendered",
function onPageRendered({ isDetailView }) {
if (!isDetailView) {
return;
}
if (!second) {
second = true;
return;
}
eventBus.off("pagerendered", onPageRendered);
resolve();
}
);
});
await page.evaluate(() => {
const container = document.getElementById("viewerContainer");
container.scrollLeft += 600;
container.scrollTop += 3000;
});
await awaitPromise(handle);
const [canvases1, canvases2] = await Promise.all([
page.evaluate(extractCanvases, 1),
page.evaluate(extractCanvases, 2),
]);
expect(canvases1.length)
.withContext(`In ${browserName}, first page`)
.toBe(2);
expect(canvases1[1].size)
.withContext(`In ${browserName}, first page`)
.toBeLessThan(4e6);
expect(canvases1[1])
.withContext(`In ${browserName}, first page`)
.toEqual(
jasmine.objectContaining({
topLeft: "#38761d", // dark green
bottomRight: "#b6d7a8", // light green
})
);
expect(canvases2.length)
.withContext(`In ${browserName}, second page`)
.toBe(2);
expect(canvases2[1].size)
.withContext(`In ${browserName}, second page`)
.toBeLessThan(4e6);
expect(canvases2[1])
.withContext(`In ${browserName}, second page`)
.toEqual(
jasmine.objectContaining({
topLeft: "#134f5c", // dark cyan
bottomRight: "#a2c4c9", // light cyan
})
);
});
});
});
describe("pagerendered event", () => {
const forEachPage = setupPages("100%", pixelRatio, -1, {
eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = [];
eventBus.on(
"pagerendered",
({ pageNumber, isDetailView, cssTransform }) => {
globalThis.__pageRenderedEvents.push({
pageNumber,
isDetailView,
cssTransform,
});
}
);
},
});
it("is dispatched properly", async () => {
await forEachPage(async (browserName, page) => {
const getPageRenderedEvents = () =>
page.evaluate(() => {
const events = globalThis.__pageRenderedEvents;
globalThis.__pageRenderedEvents = [];
return events;
});
const waitForPageRenderedEvent = filter =>
page.waitForFunction(
filterStr =>
// eslint-disable-next-line no-eval
globalThis.__pageRenderedEvents.some(eval(filterStr)),
{ polling: 50 },
String(filter)
);
// Initial render
await waitForPageRenderedEvent(e => e.pageNumber === 2);
expect(await getPageRenderedEvents())
.withContext(`In ${browserName}, initial render`)
.toEqual([
{ pageNumber: 1, isDetailView: false, cssTransform: false },
{ pageNumber: 2, isDetailView: false, cssTransform: false },
]);
// Zoom-in without triggering the detail view
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 1.05,
});
});
await waitForPageRenderedEvent(e => e.pageNumber === 2);
expect(await getPageRenderedEvents())
.withContext(`In ${browserName}, first zoom`)
.toEqual([
{ pageNumber: 1, isDetailView: false, cssTransform: false },
{ pageNumber: 2, isDetailView: false, cssTransform: false },
]);
// Zoom-in on the first page, triggering the detail view
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 2,
});
});
await Promise.all([
waitForPageRenderedEvent(
e => e.isDetailView && e.pageNumber === 1
),
waitForPageRenderedEvent(e => e.pageNumber === 2),
]);
expect(await getPageRenderedEvents())
.withContext(`In ${browserName}, second zoom`)
.toEqual([
{ pageNumber: 1, isDetailView: false, cssTransform: false },
{ pageNumber: 1, isDetailView: true, cssTransform: false },
{ pageNumber: 2, isDetailView: false, cssTransform: false },
]);
// Zoom-in more
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 2,
});
});
await Promise.all([
waitForPageRenderedEvent(
e => e.isDetailView && e.pageNumber === 1
),
waitForPageRenderedEvent(e => e.pageNumber === 2),
]);
expect(await getPageRenderedEvents())
.withContext(`In ${browserName}, third zoom`)
.toEqual([
{ pageNumber: 1, isDetailView: false, cssTransform: true },
{ pageNumber: 2, isDetailView: false, cssTransform: true },
{ pageNumber: 1, isDetailView: true, cssTransform: false },
]);
// Scroll to another area of the first page
await page.evaluate(() => {
const container = document.getElementById("viewerContainer");
container.scrollTop += 1600;
container.scrollLeft += 1100;
});
await waitForPageRenderedEvent(e => e.isDetailView);
expect(await getPageRenderedEvents())
.withContext(`In ${browserName}, first scroll`)
.toEqual([
{ pageNumber: 1, isDetailView: true, cssTransform: false },
]);
// Scroll to the second page
await page.evaluate(() => {
const container = document.getElementById("viewerContainer");
const pageElement = document.querySelector(".page");
container.scrollTop +=
pageElement.getBoundingClientRect().height;
});
await waitForPageRenderedEvent(e => e.isDetailView);
expect(await getPageRenderedEvents())
.withContext(`In ${browserName}, second scroll`)
.toEqual([
{ pageNumber: 2, isDetailView: true, cssTransform: false },
]);
// Zoom-out, to not have the detail view anymore
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 0.25,
});
});
await Promise.all([
waitForPageRenderedEvent(e => e.pageNumber === 1),
waitForPageRenderedEvent(e => e.pageNumber === 2),
]);
expect(await getPageRenderedEvents())
.withContext(`In ${browserName}, second zoom`)
.toEqual([
{ pageNumber: 2, isDetailView: false, cssTransform: false },
{ pageNumber: 1, isDetailView: false, cssTransform: false },
]);
const canvasesPerPage = await page.evaluate(() =>
Array.from(
document.querySelectorAll(".canvasWrapper"),
wrapper => wrapper.childElementCount
)
);
expect(canvasesPerPage)
.withContext(`In ${browserName}, number of canvases`)
.toEqual([1, 1]);
});
});
});
});
}
describe("when immediately cancelled and re-rendered", () => {
const forEachPage = setupPages("100%", 1, -1, {
eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = [];
eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => {
globalThis.__pageRenderedEvents.push({ pageNumber, isDetailView });
});
},
});
it("properly cleans up old canvases from the dom", async () => {
await forEachPage(async (browserName, page) => {
const waitForPageRenderedEvent = filter =>
page.waitForFunction(
filterStr => {
// eslint-disable-next-line no-eval
if (globalThis.__pageRenderedEvents.some(eval(filterStr))) {
globalThis.__pageRenderedEvents = [];
return true;
}
return false;
},
{ polling: 50 },
String(filter)
);
const getCanvasCount = () =>
page.evaluate(
() =>
document.querySelector("[data-page-number='1'] .canvasWrapper")
.childElementCount
);
await waitForPageRenderedEvent(e => e.pageNumber === 1);
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 4,
});
});
await waitForPageRenderedEvent(
e => e.pageNumber === 1 && e.isDetailView
);
expect(await getCanvasCount())
.withContext(`In ${browserName}, after the first zoom-in`)
.toBe(2);
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 0.75,
});
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 0.25,
});
});
await waitForPageRenderedEvent(e => e.pageNumber === 1);
expect(await getCanvasCount())
.withContext(`In ${browserName}, after the two zoom-out`)
.toBe(1);
});
});
});
describe("when cancelled and re-rendered after 1 microtick", () => {
const forEachPage = setupPages("100%", 1, -1, {
eventBusSetup: eventBus => {
globalThis.__pageRenderedEvents = [];
eventBus.on("pagerendered", ({ pageNumber, isDetailView }) => {
globalThis.__pageRenderedEvents.push({ pageNumber, isDetailView });
});
},
});
it("properly cleans up old canvases from the dom", async () => {
await forEachPage(async (browserName, page) => {
const waitForPageRenderedEvent = filter =>
page.waitForFunction(
filterStr => {
// eslint-disable-next-line no-eval
if (globalThis.__pageRenderedEvents.some(eval(filterStr))) {
globalThis.__pageRenderedEvents = [];
return true;
}
return false;
},
{ polling: 50 },
String(filter)
);
const getCanvasCount = () =>
page.evaluate(
() =>
document.querySelector("[data-page-number='1'] .canvasWrapper")
.childElementCount
);
await waitForPageRenderedEvent(e => e.pageNumber === 1);
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 4,
});
});
await waitForPageRenderedEvent(
e => e.pageNumber === 1 && e.isDetailView
);
expect(await getCanvasCount())
.withContext(`In ${browserName}, after the first zoom-in`)
.toBe(2);
await page.evaluate(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 0.75,
});
Promise.resolve().then(() => {
window.PDFViewerApplication.pdfViewer.updateScale({
drawingDelay: -1,
scaleFactor: 0.25,
});
});
});
await waitForPageRenderedEvent(e => e.pageNumber === 1);
expect(await getCanvasCount())
.withContext(`In ${browserName}, after the two zoom-out`)
.toBe(1);
});
});
});
});
describe("SecondaryToolbar", () => {
let pages;
function normalizeRotation(rotation) {
return ((rotation % 360) + 360) % 360;
}
function waitForRotationChanging(page, pagesRotation) {
return page.evaluateHandle(
rotation => [
new Promise(resolve => {
const { eventBus } = window.PDFViewerApplication;
eventBus.on("rotationchanging", function handler(e) {
if (rotation === undefined || e.pagesRotation === rotation) {
resolve();
eventBus.off("rotationchanging", handler);
}
});
}),
],
normalizeRotation(pagesRotation)
);
}
beforeEach(async () => {
pages = await loadAndWait("issue18694.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that the SecondaryToolbar doesn't close between rotations", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.click("#secondaryToolbarToggleButton");
await page.waitForSelector("#secondaryToolbar", { hidden: false });
for (let i = 1; i <= 4; i++) {
const secondaryToolbarIsOpen = await page.evaluate(
() => window.PDFViewerApplication.secondaryToolbar.isOpen
);
expect(secondaryToolbarIsOpen)
.withContext(`In ${browserName}`)
.toBeTrue();
const rotation = i * 90;
const handle = await waitForRotationChanging(page, rotation);
await page.click("#pageRotateCw");
await awaitPromise(handle);
const pagesRotation = await page.evaluate(
() => window.PDFViewerApplication.pdfViewer.pagesRotation
);
expect(pagesRotation)
.withContext(`In ${browserName}`)
.toBe(normalizeRotation(rotation));
}
})
);
});
});
describe("Filename with a hash sign", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("empty%23hash.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must extract the filename correctly", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const filename = await page.evaluate(() => document.title);
expect(filename)
.withContext(`In ${browserName}`)
.toBe("empty#hash.pdf");
})
);
});
});
describe("File param with an URL", () => {
let pages;
beforeEach(async () => {
const baseURL = new URL(global.integrationBaseUrl);
const url = `${baseURL.origin}/build/generic/web/compressed.tracemonkey-pldi-09.pdf`;
pages = await loadAndWait(
encodeURIComponent(url),
".textLayer .endOfContent"
);
});
afterEach(async () => {
await closePages(pages);
});
it("must load and extract the filename correctly", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const filename = await page.evaluate(() => document.title);
expect(filename)
.withContext(`In ${browserName}`)
.toBe("compressed.tracemonkey-pldi-09.pdf");
})
);
});
});
describe("Keyboard scrolling on startup (bug 843653)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait("tracemonkey.pdf", ".textLayer .endOfContent");
});
afterEach(async () => {
await closePages(pages);
});
it("must check that keyboard scrolling works without having to give the focus to the viewer", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const pdfViewer = await page.evaluateHandle(
() => window.PDFViewerApplication.pdfViewer
);
// The viewer should not have the focus.
const hasFocus = await pdfViewer.evaluate(viewer =>
viewer.container.contains(document.activeElement)
);
expect(hasFocus).withContext(`In ${browserName}`).toBeFalse();
let currentPageNumber = await pdfViewer.evaluate(
viewer => viewer.currentPageNumber
);
expect(currentPageNumber).withContext(`In ${browserName}`).toBe(1);
// Press the 'PageDown' key to check that it works.
const handle = await waitForPageChanging(page);
await page.keyboard.press("PageDown");
await awaitPromise(handle);
// The second page should be displayed.
currentPageNumber = await pdfViewer.evaluate(
viewer => viewer.currentPageNumber
);
expect(currentPageNumber).withContext(`In ${browserName}`).toBe(2);
})
);
});
});
describe("Printing can be disallowed for some pdfs (bug 1978985)", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"print_protection.pdf",
"#passwordDialog",
null,
null,
{ enablePermissions: true }
);
});
afterEach(async () => {
await closePages(pages);
});
it("must check that printing is disallowed", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await page.waitForSelector("#printButton", {
visible: true,
});
const selector = "#passwordDialog input#password";
await page.waitForSelector(selector, { visible: true });
await page.type(selector, "1234");
await page.click("#passwordDialog button#passwordSubmit");
await page.waitForSelector(".textLayer .endOfContent");
// The print button should be hidden.
await page.waitForSelector("#printButton", {
hidden: true,
});
await page.waitForSelector("#secondaryPrint", {
hidden: true,
});
const hasThrown = await page.evaluate(() => {
try {
window.print();
} catch {
return true;
}
return false;
});
expect(hasThrown).withContext(`In ${browserName}`).toBeTrue();
})
);
});
});
describe("Pinch-zoom", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
it("keeps the content under the pinch centre fixed on the screen", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
if (browserName === "firefox") {
pending(
"Touch events are not supported on devices without touch screen in Firefox."
);
}
if (browserName === "chrome") {
pending(
"Pinch zoom emulation is not supported for WebDriver BiDi in Chrome."
);
}
const rect = await getSpanRectFromText(page, 1, "type-stable");
const originX = rect.x + rect.width / 2;
const originY = rect.y + rect.height / 2;
const rendered = await createPromise(page, resolve => {
const cb = e => {
if (e.pageNumber === 1) {
window.PDFViewerApplication.eventBus.off(
"textlayerrendered",
cb
);
resolve();
}
};
window.PDFViewerApplication.eventBus.on("textlayerrendered", cb);
});
const client = await page.target().createCDPSession();
await client.send("Input.synthesizePinchGesture", {
x: originX,
y: originY,
scaleFactor: 3,
gestureSourceType: "touch",
});
await awaitPromise(rendered);
const spanHandle = await page.evaluateHandle(() =>
Array.from(
document.querySelectorAll(
'.page[data-page-number="1"] .textLayer span'
)
).find(span => span.textContent.includes("type-stable"))
);
expect(await spanHandle.isIntersectingViewport()).toBeTrue();
})
);
});
});
describe("Scroll into view", () => {
let pages;
beforeEach(async () => {
pages = await loadAndWait(
"tracemonkey_annotation_on_page_8.pdf",
`.page[data-page-number = "1"] .endOfContent`
);
});
it("Check that the top right corner of the annotation is centered vertically", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
const handle = await page.evaluateHandle(() => [
new Promise(resolve => {
const container = document.getElementById("viewerContainer");
container.addEventListener("scrollend", resolve, {
once: true,
});
window.PDFViewerApplication.pdfLinkService.goToXY(
8,
43.55,
198.36,
{
center: "vertical",
}
);
}),
]);
await awaitPromise(handle);
const annotationSelector =
".page[data-page-number='8'] .stampAnnotation";
await page.waitForSelector(annotationSelector, { visible: true });
const rect = await getRect(page, annotationSelector);
const containerRect = await getRect(page, "#viewerContainer");
expect(
Math.abs(2 * (rect.y - containerRect.y) - containerRect.height)
)
.withContext(`In ${browserName}`)
.toBeLessThan(1);
})
);
});
});
});