Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions extensions/chromium/preferences_schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@
"type": "boolean",
"default": false
},
"enableSplitMerge": {
"type": "boolean",
"default": false
},
"enableUpdatedAddImage": {
"type": "boolean",
"default": false
Expand Down
1 change: 1 addition & 0 deletions test/integration/jasmine-boot.js
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ async function runTests(results) {
"freetext_editor_spec.mjs",
"highlight_editor_spec.mjs",
"ink_editor_spec.mjs",
"reorganize_pages_spec.mjs",
"scripting_spec.mjs",
"signature_editor_spec.mjs",
"stamp_editor_spec.mjs",
Expand Down
235 changes: 235 additions & 0 deletions test/integration/reorganize_pages_spec.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
/* Copyright 2026 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,
dragAndDrop,
getRect,
getThumbnailSelector,
loadAndWait,
waitForDOMMutation,
} from "./test_utils.mjs";

async function waitForThumbnailVisible(page, pageNums) {
await page.click("#viewsManagerToggleButton");

const thumbSelector = "#thumbnailsView .thumbnailImage";
await page.waitForSelector(thumbSelector, { visible: true });
if (!pageNums) {
return null;
}
if (!Array.isArray(pageNums)) {
pageNums = [pageNums];
}
return Promise.all(
pageNums.map(pageNum =>
page.waitForSelector(getThumbnailSelector(pageNum), { visible: true })
)
);
}

function waitForPagesEdited(page) {
return createPromise(page, resolve => {
window.PDFViewerApplication.eventBus.on(
"pagesedited",
({ pagesMapper }) => {
resolve(Array.from(pagesMapper.getMapping()));
},
{
once: true,
}
);
});
}

describe("Reorganize Pages View", () => {
describe("Drag & Drop", () => {
let pages;

beforeEach(async () => {
pages = await loadAndWait(
"page_with_number.pdf",
"#viewsManagerToggleButton",
"page-fit",
null,
{ enableSplitMerge: true }
);
});

afterEach(async () => {
await closePages(pages);
});

it("should show a drag marker when dragging a thumbnail", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
const rect1 = await getRect(page, getThumbnailSelector(1));
const rect2 = await getRect(page, getThumbnailSelector(2));

const handleAddedMarker = await waitForDOMMutation(
page,
mutationList => {
for (const mutation of mutationList) {
if (mutation.type !== "childList") {
continue;
}
for (const node of mutation.addedNodes) {
if (node.classList.contains("dragMarker")) {
return true;
}
}
}
return false;
}
);
const handleRemovedMarker = await waitForDOMMutation(
page,
mutationList => {
for (const mutation of mutationList) {
if (mutation.type !== "childList") {
continue;
}
for (const node of mutation.removedNodes) {
if (node.classList.contains("dragMarker")) {
return true;
}
}
}
return false;
}
);
const dndPromise = dragAndDrop(
page,
getThumbnailSelector(1),
[[0, rect2.y - rect1.y + rect2.height / 2]],
10
);
await dndPromise;
await awaitPromise(handleAddedMarker);
await awaitPromise(handleRemovedMarker);
})
);
});

it("should reorder thumbnails after dropping", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
const rect1 = await getRect(page, getThumbnailSelector(1));
const rect2 = await getRect(page, getThumbnailSelector(2));

const handlePagesEdited = await waitForPagesEdited(page);
await dragAndDrop(
page,
getThumbnailSelector(1),
[[0, rect2.y - rect1.y + rect2.height / 2]],
10
);
const pagesMapping = await awaitPromise(handlePagesEdited);
expect(pagesMapping)
.withContext(`In ${browserName}`)
.toEqual([
2, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
]);
})
);
});

it("should reorder thumbnails after dropping at position 0", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
const rect1 = await getRect(page, getThumbnailSelector(1));
const rect2 = await getRect(page, getThumbnailSelector(2));

const handlePagesEdited = await waitForPagesEdited(page);
await dragAndDrop(
page,
getThumbnailSelector(2),
[[0, rect1.y - rect2.y - rect1.height]],
10
);
const pagesMapping = await awaitPromise(handlePagesEdited);
expect(pagesMapping)
.withContext(`In ${browserName}`)
.toEqual([
2, 1, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
]);
})
);
});

it("should reorder thumbnails after dropping two adjacent pages", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
const rect2 = await getRect(page, getThumbnailSelector(2));
const rect4 = await getRect(page, getThumbnailSelector(4));
await page.click(`.thumbnail:has(${getThumbnailSelector(1)}) input`);

const handlePagesEdited = await waitForPagesEdited(page);
await dragAndDrop(
page,
getThumbnailSelector(2),
[[0, rect4.y - rect2.y]],
10
);
const pagesMapping = await awaitPromise(handlePagesEdited);
expect(pagesMapping)
.withContext(`In ${browserName}`)
.toEqual([
3, 4, 1, 2, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17,
]);
})
);
});

it("should reorder thumbnails after dropping two non-adjacent pages", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
await waitForThumbnailVisible(page, 1);
const rect1 = await getRect(page, getThumbnailSelector(1));
const rect2 = await getRect(page, getThumbnailSelector(2));
await (await page.$(".thumbnail[page-id='14'")).scrollIntoView();
await page.waitForSelector(getThumbnailSelector(14), {
visible: true,
});
await page.click(`.thumbnail:has(${getThumbnailSelector(14)}) input`);
await (await page.$(".thumbnail[page-id='1'")).scrollIntoView();
await page.waitForSelector(getThumbnailSelector(1), {
visible: true,
});

const handlePagesEdited = await waitForPagesEdited(page);
await dragAndDrop(
page,
getThumbnailSelector(1),
[[0, rect2.y - rect1.y + rect2.height / 2]],
10
);
const pagesMapping = await awaitPromise(handlePagesEdited);
expect(pagesMapping)
.withContext(`In ${browserName}`)
.toEqual([
2, 1, 14, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 15, 16, 17,
]);
})
);
});
});
});
24 changes: 24 additions & 0 deletions test/integration/test_utils.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,24 @@ async function waitForSandboxTrip(page) {
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.
Expand Down Expand Up @@ -234,6 +252,10 @@ function getAnnotationSelector(id) {
return `[data-annotation-id="${id}"]`;
}

function getThumbnailSelector(pageNumber) {
return `.thumbnailImage[data-l10n-args='{"page":${pageNumber}}']`;
}

async function getSpanRectFromText(page, pageNumber, text) {
await page.waitForSelector(
`.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
Expand Down Expand Up @@ -957,6 +979,7 @@ export {
getSelector,
getSerialized,
getSpanRectFromText,
getThumbnailSelector,
getXY,
highlightSpan,
isCanvasMonochrome,
Expand Down Expand Up @@ -991,6 +1014,7 @@ export {
waitAndClick,
waitForAnnotationEditorLayer,
waitForAnnotationModeChanged,
waitForDOMMutation,
waitForEntryInStorage,
waitForEvent,
waitForNoElement,
Expand Down
1 change: 1 addition & 0 deletions test/pdfs/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -868,3 +868,4 @@
!bitmap.pdf
!bomb_giant.pdf
!bug2009627.pdf
!page_with_number.pdf
Binary file added test/pdfs/page_with_number.pdf
Binary file not shown.
16 changes: 16 additions & 0 deletions web/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -377,6 +377,7 @@ const PDFViewerApplication = {
enableFakeMLManager: x => x === "true",
enableGuessAltText: x => x === "true",
enablePermissions: x => x === "true",
enableSplitMerge: x => x === "true",
enableUpdatedAddImage: x => x === "true",
highlightEditorColors: x => x,
maxCanvasPixels: x => parseInt(x),
Expand Down Expand Up @@ -602,6 +603,7 @@ const PDFViewerApplication = {
pageColors,
abortSignal,
enableHWA,
enableSplitMerge: AppOptions.get("enableSplitMerge"),
});
renderingQueue.setThumbnailViewer(this.pdfThumbnailViewer);
}
Expand Down Expand Up @@ -2185,6 +2187,12 @@ const PDFViewerApplication = {
opts
);
}
eventBus._on("pagesedited", this.onPagesEdited.bind(this), opts);
eventBus._on(
"beforepagesedited",
this.onBeforePagesEdited.bind(this),
opts
);
},

bindWindowEvents() {
Expand Down Expand Up @@ -2359,6 +2367,14 @@ const PDFViewerApplication = {
await Promise.all([this.l10n?.destroy(), this.close()]);
},

onBeforePagesEdited(data) {
this.pdfViewer.onBeforePagesEdited(data);
},

onPagesEdited(data) {
this.pdfViewer.onPagesEdited(data);
},

_accumulateTicks(ticks, prop) {
// If the direction changed, reset the accumulated ticks.
if ((this[prop] > 0 && ticks < 0) || (this[prop] < 0 && ticks > 0)) {
Expand Down
5 changes: 5 additions & 0 deletions web/app_options.js
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,11 @@ const defaultOptions = {
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableSplitMerge: {
/** @type {boolean} */
value: typeof PDFJSDev === "undefined" || PDFJSDev.test("TESTING"),
kind: OptionKind.VIEWER + OptionKind.PREFERENCE,
},
enableUpdatedAddImage: {
// We'll probably want to make some experiments before enabling this
// in Firefox release, but it has to be temporary.
Expand Down
Loading
Loading