/* 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.
 */

const {
  closePages,
  getEditors,
  getEditorSelector,
  getSelectedEditors,
  getSerialized,
  loadAndWait,
  waitForEvent,
  waitForSelectedEditor,
  waitForStorageEntries,
} = require("./test_utils.js");

const copyPaste = async page => {
  let promise = waitForEvent(page, "copy");
  await page.keyboard.down("Control");
  await page.keyboard.press("c");
  await page.keyboard.up("Control");
  await promise;

  await page.waitForTimeout(10);

  promise = waitForEvent(page, "paste");
  await page.keyboard.down("Control");
  await page.keyboard.press("v");
  await page.keyboard.up("Control");
  await promise;
};

describe("FreeText Editor", () => {
  describe("FreeText", () => {
    let pages;

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

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

    it("must write a string in a FreeText editor", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("#editorFreeText");

          const rect = await page.$eval(".annotationEditorLayer", el => {
            // With Chrome something is wrong when serializing a DomRect,
            // hence we extract the values and just return them.
            const { x, y } = el.getBoundingClientRect();
            return { x, y };
          });

          const data = "Hello PDF.js World !!";
          await page.mouse.click(rect.x + 100, rect.y + 100);
          await page.type(`${getEditorSelector(0)} .internal`, data);

          const editorRect = await page.$eval(getEditorSelector(0), el => {
            const { x, y, width, height } = el.getBoundingClientRect();
            return {
              x,
              y,
              width,
              height,
            };
          });

          // Commit.
          await page.mouse.click(
            editorRect.x,
            editorRect.y + 2 * editorRect.height
          );

          await waitForSelectedEditor(page, getEditorSelector(0));
          await waitForStorageEntries(page, 1);

          const content = await page.$eval(getEditorSelector(0), el =>
            el.innerText.trimEnd()
          );
          expect(content).withContext(`In ${browserName}`).toEqual(data);
        })
      );
    });

    it("must copy/paste", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        const editorRect = await page.$eval(getEditorSelector(0), el => {
          const { x, y, width, height } = el.getBoundingClientRect();
          return { x, y, width, height };
        });

        // Select the editor created previously.
        await page.mouse.click(
          editorRect.x + editorRect.width / 2,
          editorRect.y + editorRect.height / 2
        );

        await waitForSelectedEditor(page, getEditorSelector(0));
        await copyPaste(page);
        await waitForStorageEntries(page, 2);

        const content = await page.$eval(getEditorSelector(0), el =>
          el.innerText.trimEnd()
        );

        let pastedContent = await page.$eval(getEditorSelector(1), el =>
          el.innerText.trimEnd()
        );

        expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);

        await copyPaste(page);
        await waitForStorageEntries(page, 3);

        pastedContent = await page.$eval(getEditorSelector(2), el =>
          el.innerText.trimEnd()
        );
        expect(pastedContent).withContext(`In ${browserName}`).toEqual(content);
      }
    });

    it("must clear all", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.keyboard.down("Control");
          await page.keyboard.press("a");
          await page.keyboard.up("Control");

          await page.keyboard.down("Control");
          await page.keyboard.press("Backspace");
          await page.keyboard.up("Control");

          for (const n of [0, 1, 2]) {
            const hasEditor = await page.evaluate(sel => {
              return !!document.querySelector(sel);
            }, getEditorSelector(n));

            expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);
          }

          await waitForStorageEntries(page, 0);
        })
      );
    });

    it("must check that a paste has been undone", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        const rect = await page.$eval(".annotationEditorLayer", el => {
          const { x, y } = el.getBoundingClientRect();
          return { x, y };
        });

        const data = "Hello PDF.js World !!";
        await page.mouse.click(rect.x + 100, rect.y + 100);
        await page.type(`${getEditorSelector(3)} .internal`, data);

        const editorRect = await page.$eval(getEditorSelector(3), el => {
          const { x, y, width, height } = el.getBoundingClientRect();
          return { x, y, width, height };
        });

        // Commit.
        await page.mouse.click(
          editorRect.x,
          editorRect.y + 2 * editorRect.height
        );
        // And select it again.
        await page.mouse.click(
          editorRect.x + editorRect.width / 2,
          editorRect.y + editorRect.height / 2
        );

        await waitForSelectedEditor(page, getEditorSelector(3));
        await copyPaste(page);

        let hasEditor = await page.evaluate(sel => {
          return !!document.querySelector(sel);
        }, getEditorSelector(4));

        expect(hasEditor).withContext(`In ${browserName}`).toEqual(true);

        await page.keyboard.down("Control");
        await page.keyboard.press("z");
        await page.keyboard.up("Control");
        await page.waitForTimeout(10);

        hasEditor = await page.evaluate(sel => {
          return !!document.querySelector(sel);
        }, getEditorSelector(4));

        expect(hasEditor).withContext(`In ${browserName}`).toEqual(false);

        for (let i = 0; i < 2; i++) {
          const promise = waitForEvent(page, "paste");
          await page.keyboard.down("Control");
          await page.keyboard.press("v");
          await page.keyboard.up("Control");
          await promise;
          await page.waitForTimeout(10);
        }

        let length = await page.evaluate(sel => {
          return document.querySelectorAll(sel).length;
        }, `${getEditorSelector(5)}, ${getEditorSelector(6)}`);
        expect(length).withContext(`In ${browserName}`).toEqual(2);

        for (let i = 0; i < 2; i++) {
          await page.keyboard.down("Control");
          await page.keyboard.press("z");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);
        }

        length = await page.evaluate(sel => {
          return document.querySelectorAll(sel).length;
        }, `${getEditorSelector(5)}, ${getEditorSelector(6)}`);
        expect(length).withContext(`In ${browserName}`).toEqual(0);
      }
    });

    it("must check that aria-owns is correct", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          const [stacksRect, oldAriaOwns] = await page.$eval(
            ".textLayer",
            el => {
              for (const span of el.querySelectorAll(
                `span[role="presentation"]`
              )) {
                if (span.innerText.includes("Stacks are simple to create")) {
                  span.setAttribute("pdfjs", true);
                  const { x, y, width, height } = span.getBoundingClientRect();
                  return [
                    { x, y, width, height },
                    span.getAttribute("aria-owns"),
                  ];
                }
              }
              return null;
            }
          );

          expect(oldAriaOwns).withContext(`In ${browserName}`).toEqual(null);

          const data = "Hello PDF.js World !!";
          await page.mouse.click(
            stacksRect.x + stacksRect.width + 1,
            stacksRect.y + stacksRect.height / 2
          );
          await page.type(`${getEditorSelector(7)} .internal`, data);

          // Commit.
          await page.keyboard.press("Escape");

          const ariaOwns = await page.$eval(".textLayer", el => {
            const span = el.querySelector(`span[pdfjs="true"]`);
            return span?.getAttribute("aria-owns") || null;
          });

          expect(ariaOwns.endsWith("_7-editor"))
            .withContext(`In ${browserName}`)
            .toEqual(true);
        })
      );
    });

    it("must check that right click doesn't select", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          const rect = await page.$eval(".annotationEditorLayer", el => {
            const { x, y } = el.getBoundingClientRect();
            return { x, y };
          });

          await page.keyboard.down("Control");
          await page.keyboard.press("a");
          await page.keyboard.up("Control");

          await page.keyboard.down("Control");
          await page.keyboard.press("Backspace");
          await page.keyboard.up("Control");

          const data = "Hello PDF.js World !!";
          await page.mouse.click(rect.x + 100, rect.y + 100);
          await page.type(`${getEditorSelector(8)} .internal`, data);

          const editorRect = await page.$eval(getEditorSelector(8), el => {
            const { x, y, width, height } = el.getBoundingClientRect();
            return { x, y, width, height };
          });

          // Commit.
          await page.keyboard.press("Escape");

          expect(await getSelectedEditors(page))
            .withContext(`In ${browserName}`)
            .toEqual([8]);

          await page.keyboard.press("Escape");
          expect(await getSelectedEditors(page))
            .withContext(`In ${browserName}`)
            .toEqual([]);

          await page.mouse.click(
            editorRect.x + editorRect.width / 2,
            editorRect.y + editorRect.height / 2
          );

          await waitForSelectedEditor(page, getEditorSelector(8));

          expect(await getSelectedEditors(page))
            .withContext(`In ${browserName}`)
            .toEqual([8]);

          // Escape.
          await page.keyboard.press("Escape");

          expect(await getSelectedEditors(page))
            .withContext(`In ${browserName}`)
            .toEqual([]);

          // TODO: uncomment that stuff once we've a way to dismiss
          // the context menu.
          /* await page.mouse.click(
            editorRect.x + editorRect.width / 2,
            editorRect.y + editorRect.height / 2,
            { button: "right" }
          ); */
        })
      );
    });

    it("must check that text change can be undone/redone", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        const rect = await page.$eval(".annotationEditorLayer", el => {
          const { x, y } = el.getBoundingClientRect();
          return { x, y };
        });

        await page.keyboard.down("Control");
        await page.keyboard.press("a");
        await page.keyboard.up("Control");

        await page.keyboard.down("Control");
        await page.keyboard.press("Backspace");
        await page.keyboard.up("Control");

        await page.mouse.click(rect.x + 200, rect.y + 100);

        for (let i = 0; i < 5; i++) {
          await page.type(`${getEditorSelector(9)} .internal`, "A");

          const editorRect = await page.$eval(getEditorSelector(9), el => {
            const { x, y, width, height } = el.getBoundingClientRect();
            return { x, y, width, height };
          });

          // Commit.
          await page.mouse.click(
            editorRect.x,
            editorRect.y + 2 * editorRect.height
          );

          if (i < 4) {
            // And select it again.
            await page.mouse.click(
              editorRect.x + editorRect.width / 2,
              editorRect.y + editorRect.height / 2,
              { clickCount: 2 }
            );
          }
        }

        await page.keyboard.down("Control");
        await page.keyboard.press("z");
        await page.keyboard.up("Control");
        await page.waitForTimeout(10);

        let text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
          return el.innerText;
        });

        expect(text).withContext(`In ${browserName}`).toEqual("AAAA");

        await page.keyboard.down("Control");
        await page.keyboard.press("z");
        await page.keyboard.up("Control");
        await page.waitForTimeout(10);

        text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
          return el.innerText;
        });

        expect(text).withContext(`In ${browserName}`).toEqual("AAA");

        await page.keyboard.down("Control");
        await page.keyboard.press("y");
        await page.keyboard.up("Control");
        await page.waitForTimeout(10);

        text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
          return el.innerText;
        });

        expect(text).withContext(`In ${browserName}`).toEqual("AAAA");

        for (let i = 0; i < 4; i++) {
          await page.keyboard.down("Control");
          await page.keyboard.press("z");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);
        }

        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([]);

        await page.keyboard.down("Control");
        await page.keyboard.press("y");
        await page.keyboard.up("Control");
        await page.waitForTimeout(10);

        text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
          return el.innerText;
        });

        expect(text).withContext(`In ${browserName}`).toEqual("A");

        // Add a new A.
        const editorRect = await page.$eval(getEditorSelector(9), el => {
          const { x, y, width, height } = el.getBoundingClientRect();
          return { x, y, width, height };
        });
        await page.mouse.click(
          editorRect.x + editorRect.width / 2,
          editorRect.y + editorRect.height / 2,
          { clickCount: 2 }
        );
        await page.type(`${getEditorSelector(9)} .internal`, "A");

        // Commit.
        await page.mouse.click(
          editorRect.x,
          editorRect.y + 2 * editorRect.height
        );

        text = await page.$eval(`${getEditorSelector(9)} .internal`, el => {
          return el.innerText;
        });
        expect(text).withContext(`In ${browserName}`).toEqual("AA");
      }
    });
  });

  describe("FreeText (multiselection)", () => {
    let pages;

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

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

    it("must select/unselect several editors and check copy, paste and delete operations", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        await page.click("#editorFreeText");

        const rect = await page.$eval(".annotationEditorLayer", el => {
          // With Chrome something is wrong when serializing a DomRect,
          // hence we extract the values and just return them.
          const { x, y } = el.getBoundingClientRect();
          return { x, y };
        });

        const editorCenters = [];
        for (let i = 0; i < 4; i++) {
          const data = `FreeText ${i}`;
          await page.mouse.click(
            rect.x + (i + 1) * 100,
            rect.y + (i + 1) * 100
          );
          await page.type(`${getEditorSelector(i)} .internal`, data);

          const editorRect = await page.$eval(getEditorSelector(i), el => {
            const { x, y, width, height } = el.getBoundingClientRect();
            return {
              x,
              y,
              width,
              height,
            };
          });
          editorCenters.push({
            x: editorRect.x + editorRect.width / 2,
            y: editorRect.y + editorRect.height / 2,
          });

          // Commit.
          await page.mouse.click(
            editorRect.x,
            editorRect.y + 2 * editorRect.height
          );
        }

        await page.keyboard.down("Control");
        await page.keyboard.press("a");
        await page.keyboard.up("Control");

        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([0, 1, 2, 3]);

        await page.keyboard.down("Control");
        await page.mouse.click(editorCenters[1].x, editorCenters[1].y);

        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([0, 2, 3]);

        await page.mouse.click(editorCenters[2].x, editorCenters[2].y);

        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([0, 3]);

        await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
        await page.keyboard.up("Control");

        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([0, 1, 3]);

        await copyPaste(page);

        // 0,1,3 are unselected and new pasted editors are selected.
        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([4, 5, 6]);

        // No ctrl here, hence all are unselected and 2 is selected.
        await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([2]);

        await page.mouse.click(editorCenters[1].x, editorCenters[1].y);
        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([1]);

        await page.keyboard.down("Control");

        await page.mouse.click(editorCenters[3].x, editorCenters[3].y);
        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([1, 3]);

        await page.keyboard.up("Control");

        // Delete 1 and 3.
        await page.keyboard.press("Backspace");

        await page.keyboard.down("Control");
        await page.keyboard.press("a");
        await page.keyboard.up("Control");

        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([0, 2, 4, 5, 6]);

        // Create an empty editor.
        await page.mouse.click(rect.x + 700, rect.y + 100);
        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([7]);

        // Set the focus to 2 and check that only 2 is selected.
        await page.mouse.click(editorCenters[2].x, editorCenters[2].y);
        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([2]);

        // Create an empty editor.
        await page.mouse.click(rect.x + 700, rect.y + 100);
        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([8]);
        // Dismiss it.
        await page.keyboard.press("Escape");

        // Select all.
        await page.keyboard.down("Control");
        await page.keyboard.press("a");
        await page.keyboard.up("Control");

        // Check that all the editors are correctly selected (and the focus
        // didn't move to the body when the empty editor was removed).
        expect(await getSelectedEditors(page))
          .withContext(`In ${browserName}`)
          .toEqual([0, 2, 4, 5, 6]);
      }
    });
  });

  describe("FreeText (bugs)", () => {
    let pages;

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

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

    it("must serialize invisible annotations", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("#editorFreeText");
          let currentId = 0;
          const expected = [];
          const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1);

          for (const pageNumber of oneToFourteen) {
            const pageSelector = `.page[data-page-number = "${pageNumber}"]`;

            await page.evaluate(selector => {
              const element = window.document.querySelector(selector);
              element.scrollIntoView();
            }, pageSelector);

            const annotationLayerSelector = `${pageSelector} > .annotationEditorLayer`;
            await page.waitForSelector(annotationLayerSelector, {
              visible: true,
              timeout: 0,
            });
            await page.waitForTimeout(50);
            if (![1, 14].includes(pageNumber)) {
              continue;
            }

            const rect = await page.$eval(annotationLayerSelector, el => {
              // With Chrome something is wrong when serializing a DomRect,
              // hence we extract the values and just return them.
              const { x, y } = el.getBoundingClientRect();
              return { x, y };
            });

            const data = `Hello PDF.js World !! on page ${pageNumber}`;
            expected.push(data);
            await page.mouse.click(rect.x + 100, rect.y + 100);
            await page.type(`${getEditorSelector(currentId)} .internal`, data);

            // Commit.
            await page.keyboard.press("Escape");
            await page.waitForTimeout(10);

            await waitForSelectedEditor(page, getEditorSelector(currentId));
            await waitForStorageEntries(page, currentId + 1);

            const content = await page.$eval(getEditorSelector(currentId), el =>
              el.innerText.trimEnd()
            );
            expect(content).withContext(`In ${browserName}`).toEqual(data);

            currentId += 1;
            await page.waitForTimeout(10);
          }

          const serialize = proprName =>
            page.evaluate(name => {
              const { map } =
                window.PDFViewerApplication.pdfDocument.annotationStorage
                  .serializable;
              return map ? Array.from(map.values(), x => x[name]) : [];
            }, proprName);

          expect(await serialize("value"))
            .withContext(`In ${browserName}`)
            .toEqual(expected);
          expect(await serialize("fontSize"))
            .withContext(`In ${browserName}`)
            .toEqual([10, 10]);
          expect(await serialize("color"))
            .withContext(`In ${browserName}`)
            .toEqual([
              [0, 0, 0],
              [0, 0, 0],
            ]);

          // Increase the font size for all the annotations.
          // Select all.
          await page.keyboard.down("Control");
          await page.keyboard.press("a");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);

          page.evaluate(() => {
            window.PDFViewerApplication.eventBus.dispatch(
              "switchannotationeditorparams",
              {
                source: null,
                type: /* AnnotationEditorParamsType.FREETEXT_SIZE */ 1,
                value: 13,
              }
            );
          });

          await page.waitForTimeout(10);
          expect(await serialize("fontSize"))
            .withContext(`In ${browserName}`)
            .toEqual([13, 13]);

          // Change the colors for all the annotations.
          page.evaluate(() => {
            window.PDFViewerApplication.eventBus.dispatch(
              "switchannotationeditorparams",
              {
                source: null,
                type: /* AnnotationEditorParamsType.FREETEXT_COLOR */ 2,
                value: "#FF0000",
              }
            );
          });

          await page.waitForTimeout(10);
          expect(await serialize("color"))
            .withContext(`In ${browserName}`)
            .toEqual([
              [255, 0, 0],
              [255, 0, 0],
            ]);
        })
      );
    });
  });

  describe("issue 15789", () => {
    let pages;

    beforeAll(async () => {
      pages = await loadAndWait("issue15789.pdf", ".annotationEditorLayer");
      pages = await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.select("#scaleSelect", "1");
          return [browserName, page];
        })
      );
    });

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

    it("must take the media box into account", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("#editorFreeText");
          let currentId = 0;

          for (let step = 0; step < 3; step++) {
            const rect = await page.$eval(".annotationEditorLayer", el => {
              // With Chrome something is wrong when serializing a DomRect,
              // hence we extract the values and just return them.
              const { x, y, width, height } = el.getBoundingClientRect();
              return { x, y, width, height };
            });

            const data = `Hello ${step}`;
            const x = rect.x + 0.1 * rect.width;
            const y = rect.y + 0.1 * rect.height;
            await page.mouse.click(x, y);
            await page.type(`${getEditorSelector(currentId)} .internal`, data);

            // Commit.
            await page.keyboard.press("Escape");
            await page.waitForTimeout(10);

            await page.evaluate(() => {
              document.getElementById("pageRotateCw").click();
            });
            currentId += 1;
            await page.waitForTimeout(10);
          }

          const serialize = proprName =>
            page.evaluate(name => {
              const { map } =
                window.PDFViewerApplication.pdfDocument.annotationStorage
                  .serializable;
              return map ? Array.from(map.values(), x => x[name]) : [];
            }, proprName);

          const rects = (await serialize("rect")).map(rect =>
            rect.slice(0, 2).map(x => Math.floor(x))
          );
          const expected = [
            [-28, 695],
            [-38, -10],
            [501, -20],
          ];
          // Dimensions aren't exactly the same from a platform to an other
          // so we're a bit tolerant here with the numbers.
          // Anyway the goal is to check that the bottom left corner of the
          // media box is taken into account.
          // The pdf has a media box equals to [-99 -99 612.0 792.0].
          const diffs = rects.map(
            (rect, i) =>
              Math.abs(rect[0] - expected[i][0]) < 10 &&
              Math.abs(rect[1] - expected[i][1]) < 10
          );

          expect(diffs)
            .withContext(`In ${browserName}`)
            .toEqual([true, true, true]);
        })
      );
    });
  });

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

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

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

    it("must move an annotation", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          if (browserName === "firefox") {
            pending(
              "Disabled in Firefox, because DnD isn't implemented yet (see bug 1838638)."
            );
          }

          await page.setDragInterception(true);
          await page.click("#editorFreeText");

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

          // 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([]);

          const editorRect = await page.$eval(getEditorSelector(0), el => {
            const { x, y, width, height } = el.getBoundingClientRect();
            return { x, y, width, height };
          });

          await page.mouse.dragAndDrop(
            {
              x: editorRect.x + editorRect.width / 2,
              y: editorRect.y + editorRect.height / 2,
            },
            {
              x: editorRect.x + editorRect.width / 2 + 100,
              y: editorRect.y + editorRect.height / 2 + 100,
            },
            { delay: 100 }
          );

          serialized = await getSerialized(page);
          expect(serialized.length).withContext(`In ${browserName}`).toEqual(1);
        })
      );
    });
  });

  describe("FreeText (update existing)", () => {
    let pages;

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

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

    it("must update an existing annotation", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("#editorFreeText");

          let editorIds = await getEditors(page, "freeText");
          expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);

          const editorRect = await page.$eval(getEditorSelector(0), el => {
            const { x, y, width, height } = el.getBoundingClientRect();
            return { x, y, width, height };
          });
          await page.mouse.click(
            editorRect.x + editorRect.width / 2,
            editorRect.y + editorRect.height / 2,
            { clickCount: 2 }
          );

          await page.keyboard.down("Control");
          await page.keyboard.press("End");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);

          await page.type(
            `${getEditorSelector(0)} .internal`,
            " and edited in Firefox"
          );

          // Commit.
          await page.mouse.click(
            editorRect.x,
            editorRect.y + 2 * editorRect.height
          );

          let serialized = await getSerialized(page);
          expect(serialized.length).withContext(`In ${browserName}`).toEqual(1);
          expect(serialized[0]).toEqual(
            jasmine.objectContaining({
              color: [107, 217, 41],
              fontSize: 14,
              value: "Hello World from Acrobat and edited in Firefox",
              id: "26R",
            })
          );

          // Disable editing mode.
          await page.click("#editorFreeText");
          // We want to check that the editor is displayed but not the original
          // annotation.
          editorIds = await getEditors(page, "freeText");
          expect(editorIds.length).withContext(`In ${browserName}`).toEqual(1);
          const hidden = await page.$eval(
            "[data-annotation-id='26R']",
            el => el.hidden
          );
          expect(hidden).withContext(`In ${browserName}`).toBeTrue();

          // Re-enable editing mode.
          await page.click("#editorFreeText");
          await page.focus(".annotationEditorLayer");

          // Undo.
          await page.keyboard.down("Control");
          await page.keyboard.press("z");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);

          serialized = await getSerialized(page);
          expect(serialized).withContext(`In ${browserName}`).toEqual([]);

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

          // Undo again.
          await page.keyboard.down("Control");
          await page.keyboard.press("z");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);

          // We check that the editor hasn't been removed.
          editorIds = await getEditors(page, "freeText");
          expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);
        })
      );
    });
  });

  describe("FreeText (update existing but not empty ones)", () => {
    let pages;

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

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

    it("must update an existing annotation but not an empty one", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("#editorFreeText");

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

  describe("FreeText (delete existing)", () => {
    let pages;

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

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

    it("must delete an existing annotation", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("#editorFreeText");

          let editorIds = await getEditors(page, "freeText");
          expect(editorIds.length).withContext(`In ${browserName}`).toEqual(6);

          const editorRect = await page.$eval(getEditorSelector(3), el => {
            const { x, y, width, height } = el.getBoundingClientRect();
            return { x, y, width, height };
          });
          await page.mouse.click(
            editorRect.x + editorRect.width / 2,
            editorRect.y + editorRect.height / 2
          );

          await page.keyboard.press("Backspace");
          await page.waitForTimeout(10);

          let serialized = await getSerialized(page);
          expect(serialized).toEqual([
            {
              pageIndex: 0,
              id: "51R",
              deleted: true,
            },
          ]);

          await page.click("#editorFreeText");
          // We want to check that nothing is displayed.
          editorIds = await getEditors(page, "freeText");
          expect(editorIds.length).withContext(`In ${browserName}`).toEqual(0);
          const hidden = await page.$eval(
            "[data-annotation-id='51R']",
            el => el.hidden
          );
          expect(hidden).withContext(`In ${browserName}`).toBeTrue();

          // Re-enable editing mode.
          await page.click("#editorFreeText");
          await page.focus(".annotationEditorLayer");

          // Undo.
          await page.keyboard.down("Control");
          await page.keyboard.press("z");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);

          serialized = await getSerialized(page);
          expect(serialized).withContext(`In ${browserName}`).toEqual([]);
        })
      );
    });
  });

  describe("FreeText (copy/paste existing)", () => {
    let pages;

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

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

    it("must copy and paste an existing annotation", async () => {
      // Run sequentially to avoid clipboard issues.
      for (const [browserName, page] of pages) {
        await page.click("#editorFreeText");

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

        const editorRect = await page.$eval(getEditorSelector(1), el => {
          const { x, y, width, height } = el.getBoundingClientRect();
          return { x, y, width, height };
        });
        await page.mouse.click(
          editorRect.x + editorRect.width / 2,
          editorRect.y + editorRect.height / 2
        );

        await copyPaste(page);
        await waitForStorageEntries(page, 7);
      }
    });
  });

  describe("FreeText (edit existing in double clicking on it)", () => {
    let pages;

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

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

    it("must move an annotation", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("[data-annotation-id='26R']", { clickCount: 2 });
          await page.waitForTimeout(10);

          const [focusedId, editable] = await page.evaluate(() => {
            const el = document.activeElement;
            return [el.id, el.contentEditable];
          });
          expect(focusedId)
            .withContext(`In ${browserName}`)
            .toEqual("pdfjs_internal_editor_0-editor");
          expect(editable).withContext(`In ${browserName}`).toEqual("true");

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

  describe("FreeText with popup", () => {
    let pages;

    beforeAll(async () => {
      pages = await loadAndWait(
        "annotation-freetext.pdf",
        ".annotationEditorLayer"
      );
    });

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

    it("must not remove an empty annotation", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.hover("[data-annotation-id='23R']");
          // Wait for the popup to be displayed.
          await page.waitForFunction(
            `document.querySelector("[data-annotation-id='popup_23R']").hidden === false`
          );

          // Enter in editing mode.
          await page.click("#editorFreeText");
          await page.waitForTimeout(10);
          await page.click("#editorFreeText");

          await page.hover("[data-annotation-id='23R']");
          // Wait for the popup to be displayed.
          await page.waitForFunction(
            `document.querySelector("[data-annotation-id='popup_23R']").hidden === false`
          );
        })
      );
    });

    it("must hide the popup when editing", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("[data-annotation-id='20R']");
          // Wait for the popup to be displayed.
          await page.waitForFunction(
            `document.querySelector("[data-annotation-id='popup_20R']").hidden === false`
          );

          // Enter in editing mode.
          await page.click("#editorFreeText");
          // Wait for the popup to be hidden.
          await page.waitForFunction(
            `document.querySelector("[data-annotation-id='popup_20R']").hidden === true`
          );

          // Exit editing mode.
          await page.click("#editorFreeText");
          // Wait for the popup to be visible.
          await page.waitForFunction(
            `document.querySelector("[data-annotation-id='popup_20R']").hidden === false`
          );
        })
      );
    });
  });

  describe("FreeText rotation", () => {
    let pages;

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

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

    it("must check that the dimensions of a rotated annotations are correct after a font size change", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.keyboard.press("r");
          await page.click("#editorFreeText");

          const rect = await page.$eval(".annotationEditorLayer", el => {
            const { x, y } = el.getBoundingClientRect();
            return { x, y };
          });

          const data = "Hello PDF.js World !!";
          await page.mouse.click(rect.x + 100, rect.y + 100);
          await page.type(`${getEditorSelector(0)} .internal`, data);

          const editorRect = await page.$eval(getEditorSelector(0), el => {
            const { x, y, width, height } = el.getBoundingClientRect();
            return {
              x,
              y,
              width,
              height,
            };
          });

          // Commit.
          await page.mouse.click(
            editorRect.x,
            editorRect.y + 2 * editorRect.height
          );

          let serialized = await getSerialized(page);
          let bbox = serialized[0].rect;
          let width = bbox[2] - bbox[0];
          let height = bbox[3] - bbox[1];
          expect(width < height)
            .withContext(`In ${browserName}`)
            .toEqual(true);

          for (let i = 0; i < 3; i++) {
            await page.keyboard.press("r");
            await page.waitForTimeout(10);
          }

          await page.keyboard.down("Control");
          await page.keyboard.press("a");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);

          page.evaluate(() => {
            window.PDFViewerApplication.eventBus.dispatch(
              "switchannotationeditorparams",
              {
                source: null,
                type: /* AnnotationEditorParamsType.FREETEXT_SIZE */ 1,
                value: 50,
              }
            );
          });
          await page.waitForTimeout(10);

          serialized = await getSerialized(page);
          bbox = serialized[0].rect;
          width = bbox[2] - bbox[0];
          height = bbox[3] - bbox[1];
          expect(width < height)
            .withContext(`In ${browserName}`)
            .toEqual(true);
        })
      );
    });
  });

  describe("FreeText (remove)", () => {
    let pages;

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

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

    it("must delete invisible annotations", async () => {
      await Promise.all(
        pages.map(async ([browserName, page]) => {
          await page.click("#editorFreeText");
          let currentId = 0;
          const oneToFourteen = Array.from(new Array(14).keys(), x => x + 1);

          for (const pageNumber of oneToFourteen) {
            const pageSelector = `.page[data-page-number = "${pageNumber}"]`;

            await page.evaluate(selector => {
              const element = window.document.querySelector(selector);
              element.scrollIntoView();
            }, pageSelector);

            const annotationLayerSelector = `${pageSelector} > .annotationEditorLayer`;
            await page.waitForSelector(annotationLayerSelector, {
              visible: true,
              timeout: 0,
            });
            await page.waitForTimeout(50);
            if (![1, 14].includes(pageNumber)) {
              continue;
            }

            const rect = await page.$eval(annotationLayerSelector, el => {
              // With Chrome something is wrong when serializing a DomRect,
              // hence we extract the values and just return them.
              const { x, y } = el.getBoundingClientRect();
              return { x, y };
            });

            const data = `Hello PDF.js World !! on page ${pageNumber}`;
            await page.mouse.click(rect.x + 100, rect.y + 100);
            await page.type(`${getEditorSelector(currentId)} .internal`, data);

            // Commit.
            await page.keyboard.press("Escape");
            await page.waitForTimeout(10);

            currentId += 1;
          }

          // Select all.
          await page.keyboard.down("Control");
          await page.keyboard.press("a");
          await page.keyboard.up("Control");
          await page.waitForTimeout(10);

          const serialize = () =>
            page.evaluate(() => {
              const { map } =
                window.PDFViewerApplication.pdfDocument.annotationStorage
                  .serializable;
              return map ? Array.from(map.values(), x => x.pageIndex) : [];
            });

          expect(await serialize())
            .withContext(`In ${browserName}`)
            .toEqual([0, 13]);

          // Delete
          await page.keyboard.press("Backspace");
          await page.waitForTimeout(10);

          expect(await serialize())
            .withContext(`In ${browserName}`)
            .toEqual([]);
        })
      );
    });
  });
});