diff --git a/test/integration/freetext_editor_spec.mjs b/test/integration/freetext_editor_spec.mjs index 29e89f28ae32e..564e6405f0cc3 100644 --- a/test/integration/freetext_editor_spec.mjs +++ b/test/integration/freetext_editor_spec.mjs @@ -3688,4 +3688,131 @@ describe("FreeText Editor", () => { ); }); }); + + describe("Undo annotation popup has the expected behaviour", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting a text editor can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const data = "Hello PDF.js World !!"; + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.waitForSelector(getEditorSelector(0), { + visible: true, + }); + await page.type(`${getEditorSelector(0)} .internal`, data); + + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector( + `${getEditorSelector(0)} .overlay.enabled` + ); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + await page.click('button[title="Undo"]'); + await waitForSerialized(page, 1); + await page.waitForSelector(getEditorSelector(0)); + }) + ); + }); + + it("Must check annotation popup displays the correct message", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const data = "Hello PDF.js World !!"; + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.waitForSelector(getEditorSelector(0), { + visible: true, + }); + await page.type(`${getEditorSelector(0)} .internal`, data); + + // Commit. + await page.keyboard.press("Escape"); + await page.waitForSelector( + `${getEditorSelector(0)} .overlay.enabled` + ); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction( + () => + document + .querySelector("#editorUndoBarMessage") + .textContent.trim() !== "", + { timeout: 1000 } + ); + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + expect(messageText).toContain("Text removed"); + }) + ); + }); + + it("must check that the popup disappears when a new textbox is created", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToFreeText(page); + + let rect = await getRect(page, ".annotationEditorLayer"); + const data = "Hello PDF.js World !!"; + await page.mouse.click(rect.x + 100, rect.y + 100); + await page.waitForSelector(getEditorSelector(0), { + visible: true, + }); + await page.type(`${getEditorSelector(0)} .internal`, data); + + await page.keyboard.press("Escape"); + await page.waitForSelector( + `${getEditorSelector(0)} .overlay.enabled` + ); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + rect = await getRect(page, ".annotationEditorLayer"); + const newData = "This is a new text box!"; + await page.mouse.click(rect.x + 150, rect.y + 150); + await page.waitForSelector(getEditorSelector(1), { + visible: true, + }); + await page.type(`${getEditorSelector(1)} .internal`, newData); + + await page.keyboard.press("Escape"); + await page.waitForSelector( + `${getEditorSelector(1)} .overlay.enabled` + ); + await waitForSerialized(page, 1); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + }); }); diff --git a/test/integration/highlight_editor_spec.mjs b/test/integration/highlight_editor_spec.mjs index 931ff440a4439..b0f8fe424318b 100644 --- a/test/integration/highlight_editor_spec.mjs +++ b/test/integration/highlight_editor_spec.mjs @@ -26,6 +26,7 @@ import { kbBigMoveUp, kbFocusNext, kbFocusPrevious, + kbSave, kbSelectAll, kbUndo, loadAndWait, @@ -2144,4 +2145,276 @@ describe("Highlight Editor", () => { ); }); }); + + describe("Annotation pop-up has the expected behaviour", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait( + "tracemonkey.pdf", + ".annotationEditorLayer", + null, + null, + { + highlightEditorColors: + "yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0000", + } + ); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting a highlight can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click('button[title="Undo"]'); + await waitForSerialized(page, 1); + await page.waitForSelector(getEditorSelector(0)); + await page.waitForSelector( + `.page[data-page-number = "1"] svg.highlight[fill = "#FFFF00"]` + ); + }) + ); + }); + + it("must check that the popup disappears when the undo button is clicked", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click('button[title="Undo"]'); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when the close button is clicked", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.waitForSelector("#editorUndoBarCloseButton"); + await page.click("#editorUndoBarCloseButton"); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when a new annotation is created", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + const newRect = await getSpanRectFromText(page, 1, "Introduction"); + const newX = newRect.x + newRect.width / 2; + const newY = newRect.y + newRect.height / 2; + await page.mouse.click(newX, newY, { count: 2, delay: 100 }); + + await page.waitForSelector(getEditorSelector(0)); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when the print dialog is opened", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.evaluate(() => window.print()); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when the save dialog is opened", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await kbSave(page); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must check that the popup disappears when an option from the 'more' menu is selected", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click('button[title="Tools"]'); + await page.click("#lastPage"); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + + it("must display the correct message for highlights", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + const rect = await getSpanRectFromText(page, 1, "Abstract"); + const x = rect.x + rect.width / 2; + const y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction( + () => + document + .querySelector("#editorUndoBarMessage") + .textContent.trim() !== "", + { timeout: 1000 } + ); + + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + expect(messageText).toContain("Highlight removed"); + }) + ); + }); + + it("must display correct message for multiple highlights", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToHighlight(page); + + let rect = await getSpanRectFromText(page, 1, "Abstract"); + let x = rect.x + rect.width / 2; + let y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(0)); + + rect = await getSpanRectFromText(page, 1, "Languages"); + x = rect.x + rect.width / 2; + y = rect.y + rect.height / 2; + await page.mouse.click(x, y, { count: 2, delay: 100 }); + await page.waitForSelector(getEditorSelector(1)); + + await selectAll(page); + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction( + () => + document + .querySelector("#editorUndoBarMessage") + .textContent.trim() !== "", + { timeout: 1000 } + ); + + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + + // Cleans the message text by removing all non-ASCII characters. + // It eliminates any invisible characters such as directional marks + // that interfere with string comparisons + const cleanMessage = messageText.replaceAll(/\P{ASCII}/gu, ""); + expect(cleanMessage).toContain(`2 annotations removed`); + }) + ); + }); + }); }); diff --git a/test/integration/ink_editor_spec.mjs b/test/integration/ink_editor_spec.mjs index 8cf5059dfb2ae..c1823901a0f45 100644 --- a/test/integration/ink_editor_spec.mjs +++ b/test/integration/ink_editor_spec.mjs @@ -453,4 +453,130 @@ describe("Ink Editor", () => { ); }); }); + + describe("Undo annotation popup has the expected behaviour", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting a drawing can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToInk(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const xStart = rect.x + 300; + const yStart = rect.y + 300; + const clickHandle = await waitForPointerUp(page); + await page.mouse.move(xStart, yStart); + await page.mouse.down(); + await page.mouse.move(xStart + 50, yStart + 50); + await page.mouse.up(); + await awaitPromise(clickHandle); + await commit(page); + + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + await page.click('button[title="Undo"]'); + await waitForSerialized(page, 1); + await page.waitForSelector(getEditorSelector(0)); + }) + ); + }); + + it("Must check annotation popup displays the correct message", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToInk(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const xStart = rect.x + 300; + const yStart = rect.y + 300; + const clickHandle = await waitForPointerUp(page); + await page.mouse.move(xStart, yStart); + await page.mouse.down(); + await page.mouse.move(xStart + 50, yStart + 50); + await page.mouse.up(); + await awaitPromise(clickHandle); + await commit(page); + + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction( + () => + document + .querySelector("#editorUndoBarMessage") + .textContent.trim() !== "", + { timeout: 1000 } + ); + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + expect(messageText).toContain("Drawing removed"); + }) + ); + }); + + it("must check that the popup disappears when a new drawing is created", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToInk(page); + + const rect = await getRect(page, ".annotationEditorLayer"); + const xStart = rect.x + 300; + const yStart = rect.y + 300; + const clickHandle = await waitForPointerUp(page); + await page.mouse.move(xStart, yStart); + await page.mouse.down(); + await page.mouse.move(xStart + 50, yStart + 50); + await page.mouse.up(); + await awaitPromise(clickHandle); + await commit(page); + + await page.waitForSelector(getEditorSelector(0)); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + const newRect = await getRect(page, ".annotationEditorLayer"); + const newXStart = newRect.x + 300; + const newYStart = newRect.y + 300; + const newClickHandle = await waitForPointerUp(page); + await page.mouse.move(newXStart, newYStart); + await page.mouse.down(); + await page.mouse.move(newXStart + 50, newYStart + 50); + await page.mouse.up(); + await awaitPromise(newClickHandle); + await commit(page); + + await page.waitForSelector(getEditorSelector(1)); + await waitForSerialized(page, 1); + await page.waitForSelector(getEditorSelector(1)); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + }); }); diff --git a/test/integration/stamp_editor_spec.mjs b/test/integration/stamp_editor_spec.mjs index ed706d0c0b769..c6bcc49e429cb 100644 --- a/test/integration/stamp_editor_spec.mjs +++ b/test/integration/stamp_editor_spec.mjs @@ -1281,4 +1281,98 @@ describe("Stamp Editor", () => { ); }); }); + + describe("Undo annotation popup has the expected behaviour", () => { + let pages; + + beforeEach(async () => { + pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer"); + }); + + afterEach(async () => { + await closePages(pages); + }); + + it("must check that deleting an image can be undone using the undo button", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToStamp(page); + const selector = getEditorSelector(0); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${selector} button.delete`); + await page.click(`${selector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + + await page.click('button[title="Undo"]'); + await waitForSerialized(page, 1); + await page.waitForSelector(getEditorSelector(0)); + await page.waitForSelector(`${selector} canvas`); + }) + ); + }); + + it("Must check annotation popup displays the correct message", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToStamp(page); + const selector = getEditorSelector(0); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${selector} button.delete`); + await page.click(`${selector} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForFunction( + () => + document + .querySelector("#editorUndoBarMessage") + .textContent.trim() !== "", + { timeout: 1000 } + ); + const message = await page.waitForSelector("#editorUndoBarMessage"); + const messageText = await page.evaluate( + el => el.textContent, + message + ); + expect(messageText).toContain("Image removed"); + }) + ); + }); + + it("must check that the popup disappears when a new image is inserted", async () => { + await Promise.all( + pages.map(async ([browserName, page]) => { + await switchToStamp(page); + const selector = getEditorSelector(0); + + await copyImage(page, "../images/firefox_logo.png", 0); + await page.waitForSelector(selector); + await waitForSerialized(page, 1); + + await page.waitForSelector(`${getEditorSelector(0)} button.delete`); + await page.click(`${getEditorSelector(0)} button.delete`); + await waitForSerialized(page, 0); + + await page.waitForSelector("#editorUndoBar:not([hidden])"); + await page.click("#editorStampAddImage"); + const newInput = await page.$("#stampEditorFileInput"); + await newInput.uploadFile( + `${path.join(__dirname, "../images/firefox_logo.png")}` + ); + await waitForImage(page, getEditorSelector(1)); + await waitForSerialized(page, 1); + await page.waitForSelector("#editorUndoBar", { hidden: true }); + }) + ); + }); + }); }); diff --git a/test/integration/test_utils.mjs b/test/integration/test_utils.mjs index 66673867d65cf..25cd3bb290dc3 100644 --- a/test/integration/test_utils.mjs +++ b/test/integration/test_utils.mjs @@ -741,6 +741,12 @@ async function kbFocusPrevious(page) { 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( @@ -795,6 +801,7 @@ export { kbModifierDown, kbModifierUp, kbRedo, + kbSave, kbSelectAll, kbUndo, loadAndWait,