diff --git a/test/integration/text_layer_spec.mjs b/test/integration/text_layer_spec.mjs index ca490a853..9a322c246 100644 --- a/test/integration/text_layer_spec.mjs +++ b/test/integration/text_layer_spec.mjs @@ -217,6 +217,104 @@ describe("Text layer", () => { }); }); + describe("doesn't jump when hovering on an empty area, with .markedContent", () => { + let pages; + + beforeAll(async () => { + pages = await loadAndWait( + "chrome-text-selection-markedContent.pdf", + `.page[data-page-number = "1"] .endOfContent` + ); + }); + afterAll(async () => { + await closePages(pages); + }); + + it("in per-character selection mode", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const [positionStart, positionEnd] = await Promise.all([ + getSpanRectFromText( + page, + 1, + "strengthen in the coming quarters as the railway projects under" + ).then(middlePosition), + getSpanRectFromText( + page, + 1, + "development enter the construction phase (estimated at around" + ).then(belowEndPosition), + ]); + + await page.mouse.move(positionStart.x, positionStart.y); + await page.mouse.down(); + await moveInSteps(page, positionStart, positionEnd, 20); + await page.mouse.up(); + + await expectAsync(page) + .withContext(`In ${browserName}`) + .toHaveRoughlySelected( + "rs as the railway projects under\n" + + "development enter the construction phase (estimated at " + ); + }) + ); + }); + + it("in per-word selection mode", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + const [positionStart, positionEnd] = await Promise.all([ + getSpanRectFromText( + page, + 1, + "strengthen in the coming quarters as the railway projects under" + ).then(middlePosition), + getSpanRectFromText( + page, + 1, + "development enter the construction phase (estimated at around" + ).then(belowEndPosition), + ]); + + if (browserName !== "firefox") { + await page.mouse.move(positionStart.x, positionStart.y); + await page.mouse.down({ clickCount: 1 }); + await page.mouse.up({ clickCount: 1 }); + await page.mouse.down({ clickCount: 2 }); + } else { + // When running tests with Firefox we use WebDriver BiDi, for + // which puppeteer doesn't support emulating "double click and + // hold". We need to manually dispatch an action through the + // protocol. + // See https://github.com/puppeteer/puppeteer/issues/13745. + await page.mainFrame().browsingContext.performActions([ + { + type: "pointer", + id: "__puppeteer_mouse", + actions: [ + { type: "pointerMove", ...positionStart }, + { type: "pointerDown", button: 0 }, + { type: "pointerUp", button: 0 }, + { type: "pointerDown", button: 0 }, + ], + }, + ]); + } + await moveInSteps(page, positionStart, positionEnd, 20); + await page.mouse.up(); + + await expectAsync(page) + .withContext(`In ${browserName}`) + .toHaveRoughlySelected( + "quarters as the railway projects under\n" + + "development enter the construction phase (estimated at around" + ); + }) + ); + }); + }); + describe("when selecting over a link", () => { let pages; diff --git a/test/pdfs/.gitignore b/test/pdfs/.gitignore index d3e4faf6d..ebcab4e3c 100644 --- a/test/pdfs/.gitignore +++ b/test/pdfs/.gitignore @@ -718,3 +718,4 @@ !issue19424.pdf !issue18529.pdf !issue16742.pdf +!chrome-text-selection-markedContent.pdf diff --git a/test/pdfs/chrome-text-selection-markedContent.pdf b/test/pdfs/chrome-text-selection-markedContent.pdf new file mode 100644 index 000000000..6128cab2c Binary files /dev/null and b/test/pdfs/chrome-text-selection-markedContent.pdf differ diff --git a/web/text_layer_builder.js b/web/text_layer_builder.js index 30816eef0..cee7e0bab 100644 --- a/web/text_layer_builder.js +++ b/web/text_layer_builder.js @@ -307,6 +307,14 @@ class TextLayerBuilder { if (anchor.nodeType === Node.TEXT_NODE) { anchor = anchor.parentNode; } + if (!modifyStart && range.endOffset === 0) { + do { + while (!anchor.previousSibling) { + anchor = anchor.parentNode; + } + anchor = anchor.previousSibling; + } while (!anchor.childNodes.length); + } const parentTextLayer = anchor.parentElement?.closest(".textLayer"); const endDiv = this.#textLayers.get(parentTextLayer);