Skip to content
Open
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
74 changes: 74 additions & 0 deletions test/integration/text_layer_spec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,80 @@ describe("Text layer", () => {
);
});
});

describe("when selecting text with find highlights active", () => {
let pages;

beforeEach(async () => {
pages = await loadAndWait("find_all.pdf", ".textLayer", 100);
});

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

it("doesn't jump when selection anchor is inside a highlight element", async () => {
await Promise.all(
pages.map(async ([browserName, page]) => {
// Highlight all occurrences of the letter A (case insensitive).
await page.click("#viewFindButton");
await page.waitForSelector("#findInput", { visible: true });
await page.type("#findInput", "a");
await page.click("#findHighlightAll + label");
await page.waitForSelector(".textLayer .highlight");

// find_all.pdf contains 'AB BA' in a monospace font. These are
// the glyph metrics at 100% zoom, extracted from the PDF.
const glyphWidth = 15.98;
const expectedFirstAX = 30;

// Compute the drag coordinates to select exactly "AB". The
// horizontal positions use the page origin and PDF glyph
// metrics; the vertical center comes from the highlight.
const pageDiv = await page.$(".page canvas");
const pageBox = await pageDiv.boundingBox();
const firstHighlight = await page.$(".textLayer .highlight");
const highlightBox = await firstHighlight.boundingBox();

// Drag from beginning of first 'A' to end of second 'B'
const aStart = pageBox.x + expectedFirstAX;
const startY = Math.round(
highlightBox.y + highlightBox.height / 2
);
const bEnd = Math.round(aStart + glyphWidth * 2);

await page.mouse.move(aStart, startY);
await page.mouse.down();
await moveInSteps(
page,
{ x: aStart, y: startY },
{ x: bEnd, y: startY },
20
);
await page.mouse.up();

const selection = await page.evaluate(() =>
window.getSelection().toString()
);
expect(selection).withContext(`In ${browserName}`).toEqual("AB");

// The selectionchange handler in TextLayerBuilder walks up
// from .highlight to its parent span before placing
// endOfContent (see text_layer_builder.js). Without that
// fix, endOfContent would be inserted inside the text span
// (as a sibling of the .highlight) instead of as a direct
// child of .textLayer. Verify the correct DOM structure.
const endOfContentIsDirectChild = await page.evaluate(() => {
const eoc = document.querySelector(".textLayer .endOfContent");
return eoc?.parentElement?.classList.contains("textLayer");
});
expect(endOfContentIsDirectChild)
.withContext(`In ${browserName}`)
.toBeTrue();
})
);
});
});
});

describe("using selection carets", () => {
Expand Down
3 changes: 3 additions & 0 deletions web/text_layer_builder.js
Original file line number Diff line number Diff line change
Expand Up @@ -307,6 +307,9 @@ class TextLayerBuilder {
if (anchor.nodeType === Node.TEXT_NODE) {
anchor = anchor.parentNode;
}
if (anchor.classList?.contains("highlight")) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is possible to write an integration test for this, to avoid regressions? We already have other tests for the text layer (see https://github.com/mozilla/pdf.js/blob/master/test/integration/text_layer_spec.mjs) and the search logic (see https://github.com/mozilla/pdf.js/blob/master/test/integration/find_spec.mjs) so I'd imagine there is some prior art for such a test.

Copy link
Author

@matt-atticus matt-atticus Feb 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey Tim, apologies for the slow movement on this one.

I've been hitting issues trying to get the test suite running locally, so I haven't been able to verify the results. Nevertheless, I've had a crack at an integration test for this, following the precedents set by other tests as best I could.

anchor = anchor.parentNode;
}
if (!modifyStart && range.endOffset === 0) {
do {
while (!anchor.previousSibling) {
Expand Down