/* Copyright 2022 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 {
  applyFunctionToEditor,
  awaitPromise,
  clearEditors,
  clearInput,
  closePages,
  copy,
  copyToClipboard,
  dragAndDrop,
  getAnnotationSelector,
  getEditorDimensions,
  getEditors,
  getEditorSelector,
  getFirstSerialized,
  getRect,
  getSerialized,
  isCanvasMonochrome,
  kbBigMoveDown,
  kbBigMoveRight,
  kbUndo,
  loadAndWait,
  paste,
  pasteFromClipboard,
  scrollIntoView,
  selectEditor,
  serializeBitmapDimensions,
  switchToEditor,
  unselectEditor,
  waitForAnnotationEditorLayer,
  waitForAnnotationModeChanged,
  waitForEntryInStorage,
  waitForPageRendered,
  waitForSelectedEditor,
  waitForSerialized,
  waitForTimeout,
} from "./test_utils.mjs";
import fs from "fs";
import path from "path";
import { PNG } from "pngjs";

const __dirname = import.meta.dirname;

const clearAll = clearEditors.bind(null, "stamp");

const waitForImage = async (page, selector) => {
  await page.waitForSelector(`${selector} canvas`);
  await page.waitForFunction(
    sel => {
      const canvas = document.querySelector(sel);
      const data = canvas
        .getContext("2d")
        .getImageData(0, 0, canvas.width, canvas.height);
      return data.data.some(x => x !== 0);
    },
    {},
    `${selector} canvas`
  );
  await page.waitForSelector(`${selector} .altText`);
};

const copyImage = async (page, imagePath, selector) => {
  const data = fs
    .readFileSync(path.join(__dirname, imagePath))
    .toString("base64");

  await copyToClipboard(page, { "image/png": `data:image/png;base64,${data}` });
  await pasteFromClipboard(page);

  await waitForImage(page, selector);
};

async function waitForTranslation(page) {
  return page.evaluate(async () => {
    await new Promise(resolve => {
      window.requestAnimationFrame(resolve);
    });
  });
}

const switchToStamp = switchToEditor.bind(null, "Stamp");

describe("Stamp Editor", () => {
  describe("Basic operations", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", null, {
        eventBusSetup: eventBus => {
          eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
            window.uiManager = uiManager;
          });
        },
      });
    });

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

    it("must load a PNG which is bigger than a page", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await switchToStamp(page);
          await page.click("#editorStampAddImage");

          const input = await page.$("#stampEditorFileInput");
          await input.uploadFile(
            `${path.join(__dirname, "../images/firefox_logo.png")}`
          );
          const editorSelector = getEditorSelector(0);
          await waitForImage(page, editorSelector);

          await page.waitForFunction(
            `document.getElementById("viewer-alert").textContent === "Image added"`
          );

          const { width } = await getEditorDimensions(page, editorSelector);

          // The image is bigger than the page, so it has been scaled down to
          // 75% of the page width.
          expect(width).toEqual("75%");

          const [bitmap] = await serializeBitmapDimensions(page);
          expect(bitmap.width).toEqual(512);
          expect(bitmap.height).toEqual(543);
        })
      );
    });

    it("must load a SVG", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await switchToStamp(page);
          await page.click("#editorStampAddImage");
          const input = await page.$("#stampEditorFileInput");
          await input.uploadFile(
            `${path.join(__dirname, "../images/firefox_logo.svg")}`
          );
          const editorSelector = getEditorSelector(0);
          await waitForImage(page, editorSelector);

          const { width } = await getEditorDimensions(page, editorSelector);

          expect(Math.round(parseFloat(width))).toEqual(40);

          const [bitmap] = await serializeBitmapDimensions(page);
          // The original size is 80x242 but to increase the resolution when it
          // is rasterized we scale it up by 96 / 72
          const ratio = await page.evaluate(
            () => window.pdfjsLib.PixelsPerInch.PDF_TO_CSS_UNITS
          );
          expect(Math.abs(bitmap.width - 242 * ratio) < 1).toBeTrue();
          expect(Math.abs(bitmap.height - 80 * ratio) < 1).toBeTrue();
        })
      );
    });

    it("must load a SVG, delete it and undo", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await switchToStamp(page);
          await page.click("#editorStampAddImage");
          const input = await page.$("#stampEditorFileInput");
          await input.uploadFile(
            `${path.join(__dirname, "../images/firefox_logo.svg")}`
          );
          const editorSelector = getEditorSelector(0);
          await waitForImage(page, editorSelector);
          await waitForSerialized(page, 1);

          await page.waitForSelector(`${editorSelector} button.deleteButton`);
          await page.click(`${editorSelector} button.deleteButton`);
          await waitForSerialized(page, 0);

          await kbUndo(page);
          await waitForImage(page, editorSelector);
          await waitForSerialized(page, 1);

          await waitForSelectedEditor(page, editorSelector);
        })
      );
    });
  });

  describe("Resize", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 50, {
        eventBusSetup: eventBus => {
          eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
            window.uiManager = uiManager;
          });
        },
      });
    });

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

    it("must check that an added image stay within the page", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await switchToStamp(page);
          const names = ["bottomLeft", "bottomRight", "topRight", "topLeft"];

          for (let i = 0; i < 4; i++) {
            if (i !== 0) {
              await clearAll(page);
            }

            await page.click("#editorStampAddImage");
            const input = await page.$("#stampEditorFileInput");
            await input.uploadFile(
              `${path.join(__dirname, "../images/firefox_logo.png")}`
            );
            const editorSelector = getEditorSelector(i);
            await waitForImage(page, editorSelector);
            await page.waitForSelector(`${editorSelector} .altText`);

            for (let j = 0; j < 4; j++) {
              await page.keyboard.press("Escape");
              await page.waitForSelector(`${editorSelector} .resizers.hidden`);

              const handle = await waitForAnnotationEditorLayer(page);
              await page.evaluate(() => {
                window.PDFViewerApplication.rotatePages(90);
              });
              await awaitPromise(handle);

              await page.focus(".stampEditor");
              await waitForSelectedEditor(page, editorSelector);

              await page.waitForSelector(
                `${editorSelector} .resizers:not(.hidden)`
              );

              const stampRect = await getRect(page, ".stampEditor");
              const [name, cursor] = await page.evaluate(rect => {
                const el = document.elementFromPoint(rect.x, rect.y);
                const cornerName = Array.from(el.classList).find(
                  c => c !== "resizer"
                );
                return [cornerName, window.getComputedStyle(el).cursor];
              }, stampRect);

              expect(name).withContext(`In ${browserName}`).toEqual(names[j]);
              expect(cursor)
                .withContext(`In ${browserName}`)
                .toEqual("nwse-resize");
            }

            const handle = await waitForAnnotationEditorLayer(page);
            await page.evaluate(() => {
              window.PDFViewerApplication.rotatePages(90);
            });
            await awaitPromise(handle);
          }
        })
      );
    });

    it("must check that the opposite corner doesn't move", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await switchToStamp(page);

          await page.click("#editorStampAddImage");
          const input = await page.$("#stampEditorFileInput");
          await input.uploadFile(
            `${path.join(__dirname, "../images/firefox_logo.png")}`
          );
          const editorSelector = getEditorSelector(0);
          await waitForImage(page, editorSelector);
          await page.waitForSelector(`${editorSelector} .resizer.topLeft`);
          const baseRect = await getRect(page, editorSelector);
          const bRX = baseRect.x + baseRect.width;
          const bRY = baseRect.y + baseRect.height;

          await dragAndDrop(page, `${editorSelector} .resizer.topLeft`, [
            [-10, -10],
            [20, 20],
            [-10, -10],
            [20, 20],
          ]);

          const newRect = await getRect(page, editorSelector);
          const newBRX = newRect.x + newRect.width;
          const newBRY = newRect.y + newRect.height;

          expect(Math.abs(bRX - newBRX) <= 1)
            .withContext(`In ${browserName}`)
            .toBeTrue();
          expect(Math.abs(bRY - newBRY) <= 1)
            .withContext(`In ${browserName}`)
            .toBeTrue();
        })
      );
    });
  });

  describe("Alt text dialog", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 50);
    });

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

    it("must check that the alt-text flow is correctly implemented", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);

        // Wait for the alt-text button to be visible.
        const buttonSelector = `${editorSelector} button.altText`;
        await page.waitForSelector(buttonSelector);

        // Click on the alt-text button.
        await page.click(buttonSelector);

        // Wait for the alt-text dialog to be visible.
        await page.waitForSelector("#altTextDialog", { visible: true });

        // Click on the alt-text editor.
        const textareaSelector = "#altTextDialog textarea";
        await page.click(textareaSelector);
        await page.type(textareaSelector, "Hello World");

        // Click on save button.
        const saveButtonSelector = "#altTextDialog #altTextSave";
        await page.click(saveButtonSelector);

        // Check that the canvas has an aria-describedby attribute.
        await page.waitForSelector(`${editorSelector}[aria-describedby]`);

        // Wait for the alt-text button to have the correct icon.
        await page.waitForSelector(`${buttonSelector}.done`);

        // Hover the button.
        await page.hover(buttonSelector);

        // Wait for the tooltip to be visible.
        const tooltipSelector = `${buttonSelector} .tooltip`;
        await page.waitForSelector(tooltipSelector, { visible: true });

        let tooltipText = await page.evaluate(
          sel => document.querySelector(`${sel}`).innerText,
          tooltipSelector
        );
        expect(tooltipText).toEqual("Hello World");

        // Now we change the alt-text and check that the tooltip is updated.
        const longString = "a".repeat(512);
        await page.click(buttonSelector);
        await page.waitForSelector("#altTextDialog", { visible: true });
        await page.evaluate(sel => {
          document.querySelector(`${sel}`).value = "";
        }, textareaSelector);
        await page.click(textareaSelector);
        await page.type(textareaSelector, longString);
        await page.click(saveButtonSelector);
        await page.waitForSelector(`${buttonSelector}.done`);
        await page.hover(buttonSelector);
        await page.waitForSelector(tooltipSelector, { visible: true });
        tooltipText = await page.evaluate(
          sel => document.querySelector(`${sel}`).innerText,
          tooltipSelector
        );
        expect(tooltipText).toEqual(longString);
        const dims = await page.evaluate(sel => {
          const { width, height } = document
            .querySelector(`${sel}`)
            .getBoundingClientRect();
          return { width, height };
        }, tooltipSelector);
        expect(dims.width / dims.height).toBeLessThan(2);

        // Now we just check that cancel didn't change anything.
        await page.click(buttonSelector);
        await page.waitForSelector("#altTextDialog", { visible: true });
        await page.evaluate(sel => {
          document.querySelector(`${sel}`).value = "";
        }, textareaSelector);
        await page.click(textareaSelector);
        await page.type(textareaSelector, "Hello PDF.js");
        const cancelButtonSelector = "#altTextDialog #altTextCancel";
        await page.click(cancelButtonSelector);
        await page.waitForSelector(`${buttonSelector}.done`);
        await page.hover(buttonSelector);
        await page.waitForSelector(tooltipSelector, { visible: true });
        tooltipText = await page.evaluate(
          sel => document.querySelector(`${sel}`).innerText,
          tooltipSelector
        );
        // The tooltip should still be longString.
        expect(tooltipText).toEqual(longString);

        // Now we switch to decorative.
        await page.click(buttonSelector);
        await page.waitForSelector("#altTextDialog", { visible: true });
        const decorativeSelector = "#altTextDialog #decorativeButton";
        await page.click(decorativeSelector);
        await page.click(saveButtonSelector);
        await page.waitForSelector(`${buttonSelector}.done`);
        await page.hover(buttonSelector);
        await page.waitForSelector(tooltipSelector, { visible: true });
        tooltipText = await page.evaluate(
          sel => document.querySelector(`${sel}`).innerText,
          tooltipSelector
        );
        expect(tooltipText).toEqual("Marked as decorative");

        // Now we switch back to non-decorative.
        await page.click(buttonSelector);
        await page.waitForSelector("#altTextDialog", { visible: true });
        const descriptionSelector = "#altTextDialog #descriptionButton";
        await page.click(descriptionSelector);
        await page.click(saveButtonSelector);
        await page.waitForSelector(`${buttonSelector}.done`);
        await page.hover(buttonSelector);
        await page.waitForSelector(tooltipSelector, { visible: true });
        tooltipText = await page.evaluate(
          sel => document.querySelector(`${sel}`).innerText,
          tooltipSelector
        );
        expect(tooltipText).toEqual(longString);

        // Now we remove the alt-text and check that the tooltip is removed.
        await page.click(buttonSelector);
        await page.waitForSelector("#altTextDialog", { visible: true });
        await page.evaluate(sel => {
          document.querySelector(`${sel}`).value = "";
        }, textareaSelector);
        await page.click(saveButtonSelector);
        await page.waitForSelector(`${buttonSelector}:not(.done)`);
        await page.hover(buttonSelector);
        await page.evaluate(
          sel => document.querySelector(sel) === null,
          tooltipSelector
        );

        // We check that the alt-text button works correctly with the
        // keyboard.
        const handle = await page.evaluateHandle(sel => {
          document.getElementById("viewerContainer").focus();
          return [
            new Promise(resolve => {
              setTimeout(() => {
                const el = document.querySelector(sel);
                el.addEventListener("focus", resolve, { once: true });
                el.focus({ focusVisible: true });
              }, 0);
            }),
          ];
        }, buttonSelector);
        await awaitPromise(handle);
        await (browserName === "chrome"
          ? page.waitForSelector(`${buttonSelector}:focus`)
          : page.waitForSelector(`${buttonSelector}:focus-visible`));
        await page.keyboard.press("Enter");
        await page.waitForSelector("#altTextDialog", { visible: true });
        await page.keyboard.press("Escape");
        await (browserName === "chrome"
          ? page.waitForSelector(`${buttonSelector}:focus`)
          : page.waitForSelector(`${buttonSelector}:focus-visible`));
      }
    });
  });

  describe("Resize an image with the keyboard", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 50);
    });

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

    it("must check that the dimensions change", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);

        await selectEditor(page, editorSelector);

        await page.waitForSelector(
          `${editorSelector} .resizer.topLeft[tabindex="-1"]`
        );

        const getDims = async () => {
          const [blX, blY, trX, trY] = await getFirstSerialized(
            page,
            x => x.rect
          );
          return [trX - blX, trY - blY];
        };

        const [width, height] = await getDims();

        // Press Enter to enter in resize-with-keyboard mode.
        await page.keyboard.press("Enter");

        // The resizer must become keyboard focusable.
        await page.waitForSelector(
          `${editorSelector} .resizer.topLeft[tabindex="0"]`
        );

        let prevWidth = width;
        let prevHeight = height;

        const waitForDimsChange = async (w, h) => {
          await page.waitForFunction(
            (prevW, prevH) => {
              const [x1, y1, x2, y2] =
                window.PDFViewerApplication.pdfDocument.annotationStorage.serializable.map
                  .values()
                  .next().value.rect;
              const newWidth = x2 - x1;
              const newHeight = y2 - y1;
              return newWidth !== prevW || newHeight !== prevH;
            },
            {},
            w,
            h
          );
        };

        for (let i = 0; i < 40; i++) {
          await page.keyboard.press("ArrowLeft");
          await waitForDimsChange(prevWidth, prevHeight);
          [prevWidth, prevHeight] = await getDims();
        }

        let [newWidth, newHeight] = await getDims();
        expect(newWidth > width + 30)
          .withContext(`In ${browserName}`)
          .toEqual(true);
        expect(newHeight > height + 30)
          .withContext(`In ${browserName}`)
          .toEqual(true);

        for (let i = 0; i < 4; i++) {
          await kbBigMoveRight(page);
          await waitForDimsChange(prevWidth, prevHeight);
          [prevWidth, prevHeight] = await getDims();
        }

        [newWidth, newHeight] = await getDims();
        expect(Math.abs(newWidth - width) < 2)
          .withContext(`In ${browserName}`)
          .toEqual(true);
        expect(Math.abs(newHeight - height) < 2)
          .withContext(`In ${browserName}`)
          .toEqual(true);

        // Move the focus to the next resizer.
        await page.keyboard.press("Tab");
        await page.waitForFunction(
          () => !!document.activeElement?.classList.contains("topMiddle")
        );

        for (let i = 0; i < 40; i++) {
          await page.keyboard.press("ArrowUp");
          await waitForDimsChange(prevWidth, prevHeight);
          [prevWidth, prevHeight] = await getDims();
        }

        [, newHeight] = await getDims();
        expect(newHeight > height + 50)
          .withContext(`In ${browserName}`)
          .toEqual(true);

        for (let i = 0; i < 4; i++) {
          await kbBigMoveDown(page);
          await waitForDimsChange(prevWidth, prevHeight);
          [prevWidth, prevHeight] = await getDims();
        }

        [, newHeight] = await getDims();
        expect(Math.abs(newHeight - height) < 2)
          .withContext(`In ${browserName}`)
          .toEqual(true);

        // Escape should remove the focus from the resizer.
        await page.keyboard.press("Escape");
        await page.waitForSelector(
          `${editorSelector} .resizer.topLeft[tabindex="-1"]`
        );
        await page.waitForFunction(
          () => !document.activeElement?.classList.contains("resizer")
        );
      }
    });
  });

  describe("Copy/paste from a tab to an other", () => {
    let pages1, pages2;

    beforeEach(async () => {
      pages1 = await loadAndWait("empty.pdf", ".annotationEditorLayer");
      pages2 = await loadAndWait("empty.pdf", ".annotationEditorLayer");
    });

    afterEach(async () => {
      await closePages(pages1);
      await closePages(pages2);
    });

    it("must check that the alt-text button is here when pasting in the second tab", async () => {
      // Run sequentially to avoid clipboard issues.
      for (let i = 0; i < pages1.length; i++) {
        const [, page1] = pages1[i];
        await page1.bringToFront();
        await switchToStamp(page1);

        const editorSelector = getEditorSelector(0);
        await copyImage(page1, "../images/firefox_logo.png", editorSelector);
        await copy(page1);

        const [, page2] = pages2[i];
        await page2.bringToFront();
        await switchToStamp(page2);

        await paste(page2);

        await waitForImage(page2, editorSelector);
      }
    });
  });

  describe("Undo a stamp", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
    });

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

    it("must check that a stamp can be undone", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        await page.waitForSelector(`${editorSelector} button.deleteButton`);
        await page.click(`${editorSelector} button.deleteButton`);
        await waitForSerialized(page, 0);

        await kbUndo(page);
        await waitForImage(page, editorSelector);
        await waitForSerialized(page, 1);
      }
    });
  });

  describe("Delete a stamp and undo it on another page", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
    });

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

    it("must check that a stamp can be undone", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        await page.waitForSelector(`${editorSelector} button.deleteButton`);
        await page.click(`${editorSelector} button.deleteButton`);
        await waitForSerialized(page, 0);

        const twoToFourteen = Array.from(new Array(13).keys(), n => n + 2);
        for (const pageNumber of twoToFourteen) {
          const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
          await scrollIntoView(page, pageSelector);
        }

        await kbUndo(page);
        await waitForSerialized(page, 1);

        const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n);
        for (const pageNumber of thirteenToOne) {
          const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
          await scrollIntoView(page, pageSelector);
        }

        await page.waitForSelector(`${editorSelector} canvas`);
      }
    });
  });

  describe("Delete a stamp, scroll and undo it", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("tracemonkey.pdf", ".annotationEditorLayer");
    });

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

    it("must check that a stamp can be undone", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        await page.waitForSelector(`${editorSelector} button.deleteButton`);
        await page.click(`${editorSelector} button.deleteButton`);
        await waitForSerialized(page, 0);

        const twoToOne = Array.from(new Array(13).keys(), n => n + 2).concat(
          Array.from(new Array(13).keys(), n => 13 - n)
        );
        for (const pageNumber of twoToOne) {
          const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
          await scrollIntoView(page, pageSelector);
        }

        await kbUndo(page);
        await waitForImage(page, editorSelector);
        await waitForSerialized(page, 1);
      }
    });
  });

  describe("Resize a stamp", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
    });

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

    it("must check that a resized stamp has its canvas at the right position", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        const serializedRect = await getFirstSerialized(page, x => x.rect);
        const rect = await getRect(page, ".resizer.bottomRight");
        const centerX = rect.x + rect.width / 2;
        const centerY = rect.y + rect.height / 2;

        await page.mouse.move(centerX, centerY);
        await page.mouse.down();
        await page.mouse.move(centerX - 500, centerY - 500);
        await page.mouse.up();

        await waitForEntryInStorage(
          page,
          "rect",
          serializedRect,
          (x, y) => x !== y
        );

        const canvasRect = await getRect(page, `${editorSelector} canvas`);
        const stampRect = await getRect(page, editorSelector);

        expect(
          ["x", "y", "width", "height"].every(
            key => Math.abs(canvasRect[key] - stampRect[key]) <= 10
          )
        ).toBeTrue();
      }
    });
  });

  describe("Add a stamp in odd spread mode", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait(
        "empty.pdf",
        ".annotationEditorLayer",
        null,
        null,
        {
          spreadModeOnLoad: 1,
        }
      );
    });

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

    it("must check that the stamp has its canvas at the right position", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        const canvasRect = await getRect(page, `${editorSelector} canvas`);
        const stampRect = await getRect(page, editorSelector);

        expect(
          ["x", "y", "width", "height"].every(
            key => Math.abs(canvasRect[key] - stampRect[key]) <= 10
          )
        ).toBeTrue();
      }
    });
  });

  describe("Copy and paste a stamp with an alt text", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
    });

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

    it("must check that the pasted image has an alt text", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);
        await applyFunctionToEditor(page, "pdfjs_internal_editor_0", editor => {
          editor.altTextData = {
            altText: "Hello World",
            decorative: false,
          };
        });
        await page.waitForSelector(`${editorSelector} .altText.done`);

        await copy(page);
        await paste(page);
        await page.waitForSelector(`${getEditorSelector(1)} .altText.done`);
        await waitForSerialized(page, 2);

        const serialized = await getSerialized(
          page,
          x => x.accessibilityData?.alt
        );

        expect(serialized)
          .withContext(`In ${browserName}`)
          .toEqual(["Hello World", "Hello World"]);
      }
    });
  });

  describe("New alt-text flow", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait(
        "empty.pdf",
        ".annotationEditorLayer",
        null,
        {
          eventBusSetup: eventBus => {
            eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
              window.uiManager = uiManager;
            });
            eventBus.on("reporttelemetry", ({ details }) => {
              (window.telemetry ||= []).push(structuredClone(details));
            });
          },
        },
        {
          enableAltText: true,
          enableUpdatedAddImage: true,
          enableGuessAltText: true,
        }
      );
    });

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

    it("must check the new alt text flow (part 1)", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        await switchToStamp(page);

        // Add an image.
        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        // Wait for the dialog to be visible.
        await page.waitForSelector("#newAltTextDialog", { visible: true });
        // Wait for the spinner to be visible.
        await page.waitForSelector("#newAltTextDescriptionContainer.loading");
        // Check we've the disclaimer.
        await page.waitForSelector("#newAltTextDisclaimer", { visible: true });

        // Check that the dialog has the correct title: "Edit..."
        await page.waitForFunction(
          "document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
        );

        // Check that AI guessed the correct alt text.
        await page.waitForFunction(
          `document.getElementById("newAltTextDescriptionTextarea").value ===
            "Fake alt text."`
        );

        // Check that the dialog has the correct title: "Edit..."
        await page.waitForFunction(
          "document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
        );

        // Check we've the disclaimer.
        await page.waitForSelector("#newAltTextDisclaimer", { visible: true });

        // Clear the input and check that the title changes to "Add..."
        await clearInput(
          page,
          "#newAltTextDescriptionTextarea",
          /* waitForInputEvent = */ true
        );
        await page.waitForFunction(
          "document.getElementById('newAltTextTitle').textContent.startsWith('Add')"
        );

        // Check we haven't the disclaimer.
        await page.waitForSelector("#newAltTextDisclaimer", { visible: false });

        // Add a new alt text and check that the title changes to "Edit..."
        await page.type("#newAltTextDescriptionTextarea", "Hello World");
        await page.waitForFunction(
          "document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
        );

        // Check we haven't the disclaimer after the modification.
        await page.waitForSelector("#newAltTextDisclaimer", { visible: false });

        // Click on the Not Now button.
        await page.click("#newAltTextNotNow");
        await page.waitForSelector("#newAltTextDialog", { visible: false });
        await waitForSelectedEditor(page, editorSelector);

        // Wait for the alt-text button to be visible.
        const buttonSelector = `${editorSelector} button.altText.new`;
        await page.waitForSelector(buttonSelector, { visible: true });

        await waitForTranslation(page);
        // Check the text in the button.
        let text = await page.evaluate(
          sel => document.querySelector(sel).textContent,
          buttonSelector
        );
        let ariaLabel = await page.evaluate(
          sel => document.querySelector(sel).getAttribute("aria-label"),
          buttonSelector
        );
        expect(text === ariaLabel && text)
          .withContext(`In ${browserName}`)
          .toEqual("Review alt text");

        // Unselect and select the editor and check that the badge is visible.
        await unselectEditor(page, editorSelector);
        await page.waitForSelector(".editToolbar", { visible: false });
        await page.waitForSelector(".noAltTextBadge", { visible: true });

        await page.evaluate(() => {
          window.uiManager.selectAll();
        });
        await waitForSelectedEditor(page, editorSelector);
        await page.waitForSelector(".editToolbar", { visible: true });
        await page.waitForSelector(".noAltTextBadge", { visible: false });

        // Click on the Review button.
        await page.click(buttonSelector);
        await page.waitForSelector("#newAltTextDialog", { visible: true });

        // Check that the dialog has the correct title: "Edit..."
        await page.waitForFunction(
          "document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
        );

        // Click on create automatically toggle button.
        await page.click("#newAltTextCreateAutomaticallyButton");
        await clearInput(
          page,
          "#newAltTextDescriptionTextarea",
          /* waitForInputEvent = */ true
        );

        // Save the empty text.
        await page.click("#newAltTextSave");
        await page.waitForSelector("#newAltTextDialog", { visible: false });
        await waitForSelectedEditor(page, editorSelector);
        await page.waitForSelector(buttonSelector, { visible: true });

        await waitForTranslation(page);
        // Check the text in the button.
        text = await page.evaluate(
          sel => document.querySelector(sel).textContent,
          buttonSelector
        );
        ariaLabel = await page.evaluate(
          sel => document.querySelector(sel).getAttribute("aria-label"),
          buttonSelector
        );
        expect(text === ariaLabel && text)
          .withContext(`In ${browserName}`)
          .toEqual("Missing alt text");

        // Unselect and select the editor and check that the badge is visible.
        await unselectEditor(page, editorSelector);
        await page.waitForSelector(".editToolbar", { visible: false });
        await page.waitForSelector(".noAltTextBadge", { visible: true });
        await page.evaluate(() => {
          window.uiManager.selectAll();
        });
        await waitForSelectedEditor(page, editorSelector);
        await page.waitForSelector(".editToolbar", { visible: true });
        await page.waitForSelector(".noAltTextBadge", { visible: false });

        // Click on the Review button.
        await page.click(buttonSelector);
        await page.waitForSelector("#newAltTextDialog", { visible: true });

        await page.waitForFunction(
          "document.getElementById('newAltTextTitle').textContent.startsWith('Add')"
        );
        // Add a new alt text and check that the title changes to "Edit..."
        await page.type("#newAltTextDescriptionTextarea", "Hello World");
        await page.waitForFunction(
          "document.getElementById('newAltTextTitle').textContent.startsWith('Edit')"
        );

        // Click on the Save button.
        await page.click("#newAltTextSave");
        await page.waitForSelector("#newAltTextDialog", { visible: false });

        await waitForTranslation(page);
        // Check the text in the button.
        text = await page.evaluate(
          sel => document.querySelector(sel).firstChild.textContent,
          buttonSelector
        );
        ariaLabel = await page.evaluate(
          sel => document.querySelector(sel).getAttribute("aria-label"),
          buttonSelector
        );
        expect(text === ariaLabel && text)
          .withContext(`In ${browserName}`)
          .toEqual("Alt text added");

        await page.hover(buttonSelector);

        // Wait for the tooltip to be visible.
        const tooltipSelector = `${buttonSelector} .tooltip`;
        await page.waitForSelector(tooltipSelector, { visible: true });

        const tooltipText = await page.evaluate(
          sel => document.querySelector(`${sel}`).textContent,
          tooltipSelector
        );
        expect(tooltipText).toEqual("Hello World");

        // Click on the Review button.
        await page.click(buttonSelector);
        await page.waitForSelector("#newAltTextDialog", { visible: true });
        await page.click("#newAltTextCreateAutomaticallyButton");
        await page.click("#newAltTextCancel");
        await page.waitForSelector("#newAltTextDialog", { visible: false });
      }
    });

    it("must check the new alt text flow (part 2)", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        // Add an image.
        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        // Wait for the dialog to be visible.
        await page.waitForSelector("#newAltTextDialog", { visible: true });

        // Wait for the spinner to be visible.
        await page.waitForSelector("#newAltTextDescriptionContainer.loading");

        // Check we've the disclaimer.
        await page.waitForSelector("#newAltTextDisclaimer", { visible: true });

        // Click in the textarea in order to stop the guessing.
        await page.click("#newAltTextDescriptionTextarea");
        await page.waitForFunction(() =>
          document
            .getElementById("newAltTextTitle")
            .textContent.startsWith("Add ")
        );

        // Check we haven't the disclaimer.
        await page.waitForSelector("#newAltTextDisclaimer", { visible: false });

        // Click on the Not Now button.
        await page.click("#newAltTextNotNow");
        await page.waitForSelector("#newAltTextDialog", { visible: false });
      }
    });

    it("must check the new alt text flow (part 3)", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await page.evaluate(() => {
          const { mlManager } = window.PDFViewerApplication;
          mlManager.enableGuessAltText =
            mlManager.enableAltTextModelDownload = false;
        });

        await switchToStamp(page);

        // Add an image.
        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        // Wait for the dialog to be visible.
        await page.waitForSelector("#newAltTextDialog", { visible: true });

        // Check we haven't the disclaimer.
        await page.waitForSelector("#newAltTextDisclaimer[hidden]");
      }
    });

    it("must check that the data in telemetry are correct", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        await page.evaluate(() => {
          const { mlManager } = window.PDFViewerApplication;
          mlManager.enableGuessAltText =
            mlManager.enableAltTextModelDownload = true;
        });
        await switchToStamp(page);

        // Add an image.
        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        // Wait for the dialog to be visible.
        await page.waitForSelector("#newAltTextDialog", { visible: true });

        // Check that AI guessed the correct alt text.
        await page.waitForFunction(
          `document.getElementById("newAltTextDescriptionTextarea").value ===
                    "Fake alt text."`
        );
        // Clear the input and check that the title changes to "Add..."
        await clearInput(
          page,
          "#newAltTextDescriptionTextarea",
          /* waitForInputEvent = */ true
        );
        // Save the empty text.
        await page.click("#newAltTextSave");
        await page.waitForSelector("#newAltTextDialog", { visible: false });

        // Get the telemetry data and clean.
        let telemetry = await page.evaluate(() => {
          const tel = window.telemetry;
          window.telemetry = [];
          return tel;
        });
        let saveTelemetry = telemetry.find(
          details => details.data.action === "pdfjs.image.alt_text.user_edit"
        );
        expect(saveTelemetry.data.data)
          .withContext(`In ${browserName}`)
          .toEqual({
            total_words: 3,
            words_removed: 3,
            words_added: 0,
          });

        // Click on the Review button.
        const buttonSelector = `${editorSelector} button.altText.new`;
        await page.waitForSelector(buttonSelector, { visible: true });
        await page.click(buttonSelector);
        await page.waitForSelector("#newAltTextDialog", { visible: true });

        // Add a new alt text and check that the title changes to "Edit..."
        await page.type("#newAltTextDescriptionTextarea", "Fake text alt foo.");

        // Save the empty text.
        await page.click("#newAltTextSave");
        await page.waitForSelector("#newAltTextDialog", { visible: false });

        telemetry = await page.evaluate(() => window.telemetry);
        saveTelemetry = telemetry.find(
          details => details.data.action === "pdfjs.image.alt_text.user_edit"
        );
        expect(saveTelemetry.data.data)
          .withContext(`In ${browserName}`)
          .toEqual({
            total_words: 3,
            words_removed: 0,
            words_added: 1,
          });
      }
    });
  });

  describe("New alt-text flow (bug 1920515)", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait(
        "empty.pdf",
        ".annotationEditorLayer",
        null,
        {
          eventBusSetup: eventBus => {
            eventBus.on("annotationeditoruimanager", ({ uiManager }) => {
              window.uiManager = uiManager;
            });
          },
        },
        {
          enableAltText: false,
          enableFakeMLManager: false,
          enableUpdatedAddImage: true,
          enableGuessAltText: true,
        }
      );
    });

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

    it("must check that the toggle button isn't displayed when there is no AI", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        // Add an image.
        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        // Wait for the dialog to be visible.
        await page.waitForSelector("#newAltTextDialog.noAi", { visible: true });

        // enableFakeMLManager is false, so it means that we don't have ML but
        // we're using the new flow, hence we don't want to have the toggle
        // button.
        await page.waitForSelector("#newAltTextCreateAutomatically", {
          hidden: true,
        });
      }
    });
  });

  describe("No auto-resize", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 67);
    });

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

    it("must check that a stamp editor isn't resizing itself", async () => {
      // Run sequentially to avoid clipboard issues.
      const editorSelector = getEditorSelector(0);

      for (const [, page] of pages) {
        await switchToStamp(page);

        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);
      }

      await Promise.all(
        pages.map(async ([browserName, page]) => {
          const getDims = () =>
            page.evaluate(sel => {
              const bbox = document.querySelector(sel).getBoundingClientRect();
              return `${bbox.width}::${bbox.height}`;
            }, editorSelector);
          const initialDims = await getDims();
          for (let i = 0; i < 50; i++) {
            // We want to make sure that the editor doesn't resize itself, so we
            // check every 10ms that the dimensions are the same.

            // eslint-disable-next-line no-restricted-syntax
            await waitForTimeout(10);

            const dims = await getDims();
            expect(dims).withContext(`In ${browserName}`).toEqual(initialDims);
          }
        })
      );
    });
  });

  describe("A stamp mustn't be on top of the secondary toolbar", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer", 600);
    });

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

    it("must check that a stamp editor isn't on top of the secondary toolbar", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/red.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);
      }

      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("#secondaryToolbarToggleButton");
          await page.waitForSelector("#secondaryToolbar", { visible: true });
          const secondary = await page.$("#secondaryToolbar");
          const png = await secondary.screenshot({ type: "png" });
          const secondaryImage = PNG.sync.read(Buffer.from(png));
          const buffer = new Uint32Array(secondaryImage.data.buffer);
          expect(buffer.every(x => x === 0xff0000ff))
            .withContext(`In ${browserName}`)
            .toBeFalse();
        })
      );
    });
  });

  describe("Stamp (move existing)", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("stamps.pdf", getAnnotationSelector("25R"));
    });

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

    it("must move an annotation", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          const modeChangedHandle = await waitForAnnotationModeChanged(page);
          await page.click(getAnnotationSelector("25R"), { count: 2 });
          await awaitPromise(modeChangedHandle);
          const editorSelector = getEditorSelector(0);
          await waitForSelectedEditor(page, editorSelector);

          const editorIds = await getEditors(page, "stamp");
          expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);

          // All the current annotations should be serialized as null objects
          // because they haven't been edited yet.
          const serialized = await getSerialized(page);
          expect(serialized).withContext(`In ${browserName}`).toEqual([]);

          // Select the annotation we want to move.
          await selectEditor(page, editorSelector);

          await dragAndDrop(page, editorSelector, [[100, 100]]);
          await waitForSerialized(page, 1);
        })
      );
    });
  });

  describe("Stamp (change alt-text)", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("stamps.pdf", getAnnotationSelector("58R"));
    });

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

    it("must update an existing alt-text", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          const modeChangedHandle = await waitForAnnotationModeChanged(page);
          await page.click(getAnnotationSelector("58R"), { count: 2 });
          await awaitPromise(modeChangedHandle);
          const editorSelector = getEditorSelector(4);
          await waitForSelectedEditor(page, editorSelector);

          const editorIds = await getEditors(page, "stamp");
          expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);

          await page.click(`${editorSelector} button.altText`);
          await page.waitForSelector("#altTextDialog", { visible: true });

          const textareaSelector = "#altTextDialog textarea";
          await page.waitForFunction(
            sel => document.querySelector(sel).value !== "",
            {},
            textareaSelector
          );

          const altText = await page.evaluate(
            sel => document.querySelector(sel).value,
            textareaSelector
          );
          expect(altText).toEqual("An elephant");

          await page.evaluate(sel => {
            document.querySelector(sel).value = "";
          }, textareaSelector);

          await page.click(textareaSelector);
          await page.type(textareaSelector, "Hello World");

          // All the current annotations should be serialized as null objects
          // because they haven't been edited yet.
          const serialized = await getSerialized(page);
          expect(serialized).withContext(`In ${browserName}`).toEqual([]);

          const saveButtonSelector = "#altTextDialog #altTextSave";
          await page.click(saveButtonSelector);

          await waitForSerialized(page, 1);
        })
      );
    });
  });

  describe("Stamp (delete existing and undo)", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("stamps.pdf", getAnnotationSelector("37R"));
    });

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

    it("must check that the annotation is correctly restored", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          const modeChangedHandle = await waitForAnnotationModeChanged(page);
          await page.click(getAnnotationSelector("37R"), { count: 2 });
          await awaitPromise(modeChangedHandle);
          const editorSelector = getEditorSelector(2);
          await waitForSelectedEditor(page, editorSelector);

          const editorIds = await getEditors(page, "stamp");
          expect(editorIds.length).withContext(`In ${browserName}`).toEqual(5);

          // All the current annotations should be serialized as null objects
          // because they haven't been edited yet.
          let serialized = await getSerialized(page);
          expect(serialized).withContext(`In ${browserName}`).toEqual([]);

          await page.waitForSelector(`${editorSelector} button.deleteButton`);
          await page.click(`${editorSelector} button.deleteButton`);

          await waitForSerialized(page, 1);
          serialized = await getSerialized(page);
          expect(serialized)
            .withContext(`In ${browserName}`)
            .toEqual([
              { id: "37R", deleted: true, pageIndex: 0, popupRef: "44R" },
            ]);

          await kbUndo(page);
          await waitForSerialized(page, 0);

          await waitForSelectedEditor(page, editorSelector);
        })
      );
    });
  });

  describe("Drag a stamp annotation and click on a touchscreen", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
    });

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

    it("must check that the annotation isn't unselected when an other finger taps on the screen", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        const stampRect = await getRect(page, editorSelector);

        await page.touchscreen.tap(stampRect.x + 10, stampRect.y + 10);
        await waitForSelectedEditor(page, editorSelector);

        await page.touchscreen.touchStart(stampRect.x + 10, stampRect.y + 10);
        await page.touchscreen.touchMove(stampRect.x + 20, stampRect.y + 20);
        await page.touchscreen.tap(stampRect.x - 10, stampRect.y - 10);
        await page.touchscreen.touchEnd();

        await waitForSelectedEditor(page, editorSelector);
      }
    });
  });

  describe("Undo deletion 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 () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        await page.waitForSelector(`${editorSelector} button.deleteButton`);
        await page.click(`${editorSelector} button.deleteButton`);
        await waitForSerialized(page, 0);
        await page.waitForSelector("#editorUndoBar", { visible: true });

        await page.waitForSelector("#editorUndoBarUndoButton", {
          visible: true,
        });
        await page.click("#editorUndoBarUndoButton");
        await waitForSerialized(page, 1);
        await page.waitForSelector(editorSelector);
        await page.waitForSelector(`${editorSelector} canvas`);
      }
    });

    it("must check that the undo deletion popup displays the correct message", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        await page.waitForSelector(`${editorSelector} button.deleteButton`);
        await page.click(`${editorSelector} button.deleteButton`);
        await waitForSerialized(page, 0);

        await page.waitForFunction(() => {
          const messageElement = document.querySelector(
            "#editorUndoBarMessage"
          );
          return messageElement && messageElement.textContent.trim() !== "";
        });
        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 () => {
      // Run sequentially to avoid clipboard issues.
      for (const [, page] of pages) {
        await switchToStamp(page);

        const editorSelector = getEditorSelector(0);
        await copyImage(page, "../images/firefox_logo.png", editorSelector);
        await page.waitForSelector(editorSelector);
        await waitForSerialized(page, 1);

        await page.waitForSelector(`${editorSelector} button.deleteButton`);
        await page.click(`${editorSelector} button.deleteButton`);
        await waitForSerialized(page, 0);

        await page.waitForSelector("#editorUndoBar", { visible: true });
        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 });
      }
    });
  });

  describe("Switch to edit mode a pdf with an existing stamp annotation on an invisible and rendered page", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("issue19239.pdf", ".annotationEditorLayer");
    });

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

    it("must move on the second page", async () => {
      await Promise.all(
        pages.map(async ([, page]) => {
          const pageOneSelector = `.page[data-page-number = "1"]`;
          const pageTwoSelector = `.page[data-page-number = "2"]`;
          await scrollIntoView(page, pageTwoSelector);
          await page.waitForSelector(pageOneSelector, { visible: false });

          await switchToStamp(page);
          await scrollIntoView(page, pageOneSelector);
          await page.waitForSelector(
            `${pageOneSelector} .annotationEditorLayer canvas`,
            { visible: true }
          );
        })
      );
    });
  });

  describe("Switch to edit mode a pdf with an existing stamp annotation on an invisible and unrendered page", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("issue19239.pdf", ".annotationEditorLayer");
    });

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

    it("must move on the last page", async () => {
      await Promise.all(
        pages.map(async ([, page]) => {
          const twoToFourteen = Array.from(new Array(13).keys(), n => n + 2);
          for (const pageNumber of twoToFourteen) {
            const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
            await scrollIntoView(page, pageSelector);
          }

          await switchToStamp(page);

          const thirteenToOne = Array.from(new Array(13).keys(), n => 13 - n);
          for (const pageNumber of thirteenToOne) {
            const pageSelector = `.page[data-page-number = "${pageNumber}"]`;
            await scrollIntoView(page, pageSelector);
          }

          await page.waitForSelector(
            `.page[data-page-number = "1"] .annotationEditorLayer canvas`,
            { visible: true }
          );
        })
      );
    });
  });

  describe("Switch to edit mode by double clicking on an existing stamp annotation", () => {
    const annotationSelector = getAnnotationSelector("999R");

    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("issue19239.pdf", annotationSelector);
    });

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

    it("must switch to edit mode", async () => {
      await Promise.all(
        pages.map(async ([, page]) => {
          await page.waitForSelector(annotationSelector);
          await scrollIntoView(page, annotationSelector);

          await page.click(annotationSelector, { count: 2 });

          await page.waitForFunction(() =>
            document
              .querySelector(".annotationEditorLayer")
              .classList.contains("stampEditing")
          );
        })
      );
    });
  });

  describe("Switch to edit mode, zoom and check that the non-editable stamp is still there", () => {
    const annotationSelector = getAnnotationSelector("14R");

    let pages;

    beforeEach(async () => {
      pages = await loadAndWait("red_stamp.pdf", annotationSelector, 20);
    });

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

    it("must check if the canvas is still red", async () => {
      await Promise.all(
        pages.map(async ([, page]) => {
          expect(
            await isCanvasMonochrome(page, 1, null, 0xff0000ff)
          ).toBeTrue();

          await switchToStamp(page);

          expect(
            await isCanvasMonochrome(page, 1, null, 0xff0000ff)
          ).toBeTrue();

          const rectPage = await getRect(
            page,
            `.page[data-page-number = "1"] .annotationEditorLayer`
          );

          const handle = await waitForPageRendered(page, 1);
          const originX = rectPage.x + rectPage.width / 2;
          const originY = rectPage.y + rectPage.height / 2;
          await page.evaluate(
            origin => {
              window.PDFViewerApplication.pdfViewer.increaseScale({
                scaleFactor: 2,
                origin,
              });
            },
            [originX, originY]
          );
          await awaitPromise(handle);

          expect(
            await isCanvasMonochrome(page, 1, null, 0xff0000ff)
          ).toBeTrue();
        })
      );
    });
  });

  describe("Stamp (move between pages)", () => {
    let pages;

    beforeEach(async () => {
      pages = await loadAndWait(
        "firefox_stamp.pdf",
        getAnnotationSelector("24R"),
        "50"
      );
    });

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

    it("must move a stamp annotation from page 1 to page 2", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await waitForPageRendered(page, 1);
          await waitForPageRendered(page, 2);
          await waitForAnnotationEditorLayer(page, 1);
          await waitForAnnotationEditorLayer(page, 2);

          const modeChangedHandle = await waitForAnnotationModeChanged(page);

          await page.click(getAnnotationSelector("24R"), { count: 2 });
          await awaitPromise(modeChangedHandle);

          const editorSelector = getEditorSelector(0);
          await waitForSelectedEditor(page, editorSelector);

          await scrollIntoView(
            page,
            `.page[data-page-number="2"] .annotationEditorLayer`
          );

          const editorRect = await getRect(page, editorSelector);
          const page2Rect = await getRect(
            page,
            `.page[data-page-number="2"] .annotationEditorLayer`
          );

          const deltaX =
            page2Rect.x +
            page2Rect.width / 2 -
            (editorRect.x + editorRect.width / 2);
          const deltaY =
            page2Rect.y +
            page2Rect.height / 3 -
            (editorRect.y + editorRect.height / 2);

          await dragAndDrop(page, editorSelector, [[deltaX, deltaY]], 10);

          await page.waitForFunction(
            sel => {
              const editorDiv = document.querySelector(sel);
              const pageDiv = editorDiv?.closest(".page");
              return pageDiv?.getAttribute("data-page-number") === "2";
            },
            {},
            editorSelector
          );
        })
      );
    });
  });
});
