first commit
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				Types tests / Test (lts/*) (push) Has been cancelled
				
			
		
			
				
	
				Lint / Lint (lts/*) (push) Has been cancelled
				
			
		
			
				
	
				CodeQL / Analyze (javascript) (push) Has been cancelled
				
			
		
			
				
	
				CI / Test (20) (push) Has been cancelled
				
			
		
			
				
	
				CI / Test (22) (push) Has been cancelled
				
			
		
			
				
	
				CI / Test (24) (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	Types tests / Test (lts/*) (push) Has been cancelled
				
			Lint / Lint (lts/*) (push) Has been cancelled
				
			CodeQL / Analyze (javascript) (push) Has been cancelled
				
			CI / Test (20) (push) Has been cancelled
				
			CI / Test (22) (push) Has been cancelled
				
			CI / Test (24) (push) Has been cancelled
				
			This commit is contained in:
		
							
								
								
									
										305
									
								
								test/integration/accessibility_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										305
									
								
								test/integration/accessibility_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,305 @@
 | 
			
		||||
/* Copyright 2021 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 {
 | 
			
		||||
  awaitPromise,
 | 
			
		||||
  closePages,
 | 
			
		||||
  loadAndWait,
 | 
			
		||||
  waitForPageRendered,
 | 
			
		||||
} from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
const isStructTreeVisible = async page => {
 | 
			
		||||
  await page.waitForSelector(".structTree");
 | 
			
		||||
  return page.evaluate(() => {
 | 
			
		||||
    let elem = document.querySelector(".structTree");
 | 
			
		||||
    while (elem) {
 | 
			
		||||
      if (elem.getAttribute("aria-hidden") === "true") {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      elem = elem.parentElement;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe("accessibility", () => {
 | 
			
		||||
  describe("structure tree", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("structure_simple.pdf", ".structTree");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must build structure that maps to text layer", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          expect(await isStructTreeVisible(page))
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeTrue();
 | 
			
		||||
 | 
			
		||||
          // Check the headings match up.
 | 
			
		||||
          const head1 = await page.$eval(
 | 
			
		||||
            ".structTree [role='heading'][aria-level='1'] span",
 | 
			
		||||
            el =>
 | 
			
		||||
              document.getElementById(el.getAttribute("aria-owns")).textContent
 | 
			
		||||
          );
 | 
			
		||||
          expect(head1).withContext(`In ${browserName}`).toEqual("Heading 1");
 | 
			
		||||
          const head2 = await page.$eval(
 | 
			
		||||
            ".structTree [role='heading'][aria-level='2'] span",
 | 
			
		||||
            el =>
 | 
			
		||||
              document.getElementById(el.getAttribute("aria-owns")).textContent
 | 
			
		||||
          );
 | 
			
		||||
          expect(head2).withContext(`In ${browserName}`).toEqual("Heading 2");
 | 
			
		||||
 | 
			
		||||
          // Check the order of the content.
 | 
			
		||||
          const texts = await page.$$eval(".structTree [aria-owns]", nodes =>
 | 
			
		||||
            nodes.map(
 | 
			
		||||
              el =>
 | 
			
		||||
                document.getElementById(el.getAttribute("aria-owns"))
 | 
			
		||||
                  .textContent
 | 
			
		||||
            )
 | 
			
		||||
          );
 | 
			
		||||
          expect(texts)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual([
 | 
			
		||||
              "Heading 1",
 | 
			
		||||
              "This paragraph 1.",
 | 
			
		||||
              "Heading 2",
 | 
			
		||||
              "This paragraph 2.",
 | 
			
		||||
            ]);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the struct tree is still there after zooming", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          for (let i = 0; i < 8; i++) {
 | 
			
		||||
            expect(await isStructTreeVisible(page))
 | 
			
		||||
              .withContext(`In ${browserName}`)
 | 
			
		||||
              .toBeTrue();
 | 
			
		||||
 | 
			
		||||
            const handle = await waitForPageRendered(page);
 | 
			
		||||
            await page.click(`#zoom${i < 4 ? "In" : "Out"}Button`);
 | 
			
		||||
            await awaitPromise(handle);
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Annotation", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "tracemonkey_a11y.pdf",
 | 
			
		||||
        ".textLayer .endOfContent"
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    function getSpans(page) {
 | 
			
		||||
      return page.evaluate(() => {
 | 
			
		||||
        const elements = document.querySelectorAll(
 | 
			
		||||
          `.textLayer span[aria-owns]:not([role="presentation"])`
 | 
			
		||||
        );
 | 
			
		||||
        const results = [];
 | 
			
		||||
        for (const element of elements) {
 | 
			
		||||
          results.push(element.innerText);
 | 
			
		||||
        }
 | 
			
		||||
        return results;
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    it("must check that some spans are linked to some annotations thanks to aria-owns", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const spanContents = await getSpans(page);
 | 
			
		||||
 | 
			
		||||
          expect(spanContents)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(["Languages", "@intel.com", "Abstract", "Introduction"]);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Annotations order", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("fields_order.pdf", ".annotationLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the text fields are in the visual order", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const ids = await page.evaluate(() => {
 | 
			
		||||
            const elements = document.querySelectorAll(
 | 
			
		||||
              ".annotationLayer .textWidgetAnnotation"
 | 
			
		||||
            );
 | 
			
		||||
            const results = [];
 | 
			
		||||
            for (const element of elements) {
 | 
			
		||||
              results.push(element.getAttribute("data-annotation-id"));
 | 
			
		||||
            }
 | 
			
		||||
            return results;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          expect(ids)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(["32R", "30R", "31R", "34R", "29R", "33R"]);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Stamp annotation accessibility", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("tagged_stamp.pdf", ".annotationLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check the id in aria-controls", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.waitForSelector(".annotationLayer");
 | 
			
		||||
          const stampId = "pdfjs_internal_id_20R";
 | 
			
		||||
          await page.click(`#${stampId}`);
 | 
			
		||||
 | 
			
		||||
          const controlledId = await page.$eval(
 | 
			
		||||
            "#pdfjs_internal_id_21R",
 | 
			
		||||
            el => document.getElementById(el.getAttribute("aria-controls")).id
 | 
			
		||||
          );
 | 
			
		||||
          expect(controlledId)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(stampId);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check the aria-label linked to the stamp annotation", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.waitForSelector(".annotationLayer");
 | 
			
		||||
 | 
			
		||||
          const ariaLabel = await page.$eval(
 | 
			
		||||
            ".annotationLayer section[role='img']",
 | 
			
		||||
            el => el.getAttribute("aria-label")
 | 
			
		||||
          );
 | 
			
		||||
          expect(ariaLabel)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual("Secondary text for stamp");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the stamp annotation is linked to the struct tree", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.waitForSelector(".structTree");
 | 
			
		||||
 | 
			
		||||
          const isLinkedToStampAnnotation = await page.$eval(
 | 
			
		||||
            ".structTree [role='figure']",
 | 
			
		||||
            el =>
 | 
			
		||||
              document
 | 
			
		||||
                .getElementById(el.getAttribute("aria-owns"))
 | 
			
		||||
                .classList.contains("stampAnnotation")
 | 
			
		||||
          );
 | 
			
		||||
          expect(isLinkedToStampAnnotation)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Figure in the content stream", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("bug1708040.pdf", ".textLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that an image is correctly inserted in the text layer", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          expect(await isStructTreeVisible(page))
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeTrue();
 | 
			
		||||
 | 
			
		||||
          const spanId = await page.evaluate(() => {
 | 
			
		||||
            const el = document.querySelector(
 | 
			
		||||
              `.structTree span[role="figure"]`
 | 
			
		||||
            );
 | 
			
		||||
            return el.getAttribute("aria-owns") || null;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          expect(spanId).withContext(`In ${browserName}`).not.toBeNull();
 | 
			
		||||
 | 
			
		||||
          const ariaLabel = await page.evaluate(id => {
 | 
			
		||||
            const img = document.querySelector(`#${id} > span[role="img"]`);
 | 
			
		||||
            return img.getAttribute("aria-label");
 | 
			
		||||
          }, spanId);
 | 
			
		||||
 | 
			
		||||
          expect(ariaLabel)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual("A logo of a fox and a globe");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("No undefined id", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("issue20102.pdf", ".textLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that span hasn't an 'undefined' id", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const id = await page.$eval("span.markedContent", span => span.id);
 | 
			
		||||
          expect(id).withContext(`In ${browserName}`).toBe("");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										881
									
								
								test/integration/annotation_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										881
									
								
								test/integration/annotation_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,881 @@
 | 
			
		||||
/* Copyright 2020 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 {
 | 
			
		||||
  closePages,
 | 
			
		||||
  getAnnotationSelector,
 | 
			
		||||
  getQuerySelector,
 | 
			
		||||
  getRect,
 | 
			
		||||
  getSelector,
 | 
			
		||||
  loadAndWait,
 | 
			
		||||
} from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
describe("Annotation highlight", () => {
 | 
			
		||||
  describe("annotation-highlight.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "annotation-highlight.pdf",
 | 
			
		||||
        getAnnotationSelector("19R")
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check the popup position in the DOM", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const highlightSelector = getAnnotationSelector("19R");
 | 
			
		||||
          const popupSelector = getAnnotationSelector("21R");
 | 
			
		||||
          const areSiblings = await page.evaluate(
 | 
			
		||||
            (highlightSel, popupSel) => {
 | 
			
		||||
              const highlight = document.querySelector(highlightSel);
 | 
			
		||||
              const popup = document.querySelector(popupSel);
 | 
			
		||||
              return highlight.nextElementSibling === popup;
 | 
			
		||||
            },
 | 
			
		||||
            highlightSelector,
 | 
			
		||||
            popupSelector
 | 
			
		||||
          );
 | 
			
		||||
          expect(areSiblings).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must show a popup on mouseover", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          let hidden = await page.$eval(
 | 
			
		||||
            getAnnotationSelector("21R"),
 | 
			
		||||
            el => el.hidden
 | 
			
		||||
          );
 | 
			
		||||
          expect(hidden).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
          await page.hover(getAnnotationSelector("19R"));
 | 
			
		||||
          await page.waitForSelector(getAnnotationSelector("21R"), {
 | 
			
		||||
            visible: true,
 | 
			
		||||
            timeout: 0,
 | 
			
		||||
          });
 | 
			
		||||
          hidden = await page.$eval(
 | 
			
		||||
            getAnnotationSelector("21R"),
 | 
			
		||||
            el => el.hidden
 | 
			
		||||
          );
 | 
			
		||||
          expect(hidden).withContext(`In ${browserName}`).toEqual(false);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Check that widget annotations are in front of highlight ones", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("bug1883609.pdf", getAnnotationSelector("23R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must click on widget annotations", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          for (const i of [23, 22, 14]) {
 | 
			
		||||
            await page.click(getAnnotationSelector(`${i}R`));
 | 
			
		||||
            await page.waitForSelector(`#pdfjs_internal_id_${i}R:focus`);
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("Checkbox annotation", () => {
 | 
			
		||||
  describe("issue12706.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("issue12706.pdf", getAnnotationSelector("63R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must let checkboxes with the same name behave like radio buttons", async () => {
 | 
			
		||||
      const selectors = [63, 70, 79].map(n => getAnnotationSelector(`${n}R`));
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          for (const selector of selectors) {
 | 
			
		||||
            await page.click(selector);
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector} > :first-child').checked`
 | 
			
		||||
            );
 | 
			
		||||
 | 
			
		||||
            for (const otherSelector of selectors) {
 | 
			
		||||
              const checked = await page.$eval(
 | 
			
		||||
                `${otherSelector} > :first-child`,
 | 
			
		||||
                el => el.checked
 | 
			
		||||
              );
 | 
			
		||||
              expect(checked)
 | 
			
		||||
                .withContext(`In ${browserName}`)
 | 
			
		||||
                .toBe(selector === otherSelector);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("issue15597.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("issue15597.pdf", getAnnotationSelector("7R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check the checkbox", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const selector = getAnnotationSelector("7R");
 | 
			
		||||
          await page.click(selector);
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector('${selector} > :first-child').checked`
 | 
			
		||||
          );
 | 
			
		||||
          expect(true).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("bug1847733.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("bug1847733.pdf", getAnnotationSelector("18R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check the checkbox", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const selectors = [18, 30, 42, 54].map(id =>
 | 
			
		||||
            getAnnotationSelector(`${id}R`)
 | 
			
		||||
          );
 | 
			
		||||
          for (const selector of selectors) {
 | 
			
		||||
            await page.click(selector);
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector} > :first-child').checked`
 | 
			
		||||
            );
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("Text widget", () => {
 | 
			
		||||
  describe("issue13271.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("issue13271.pdf", getAnnotationSelector("24R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must update all the fields with the same value", async () => {
 | 
			
		||||
      const base = "hello world";
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.type(getSelector("25R"), base);
 | 
			
		||||
          await page.waitForFunction(`${getQuerySelector("24R")}.value !== ""`);
 | 
			
		||||
          await page.waitForFunction(`${getQuerySelector("26R")}.value !== ""`);
 | 
			
		||||
 | 
			
		||||
          let text = await page.$eval(getSelector("24R"), el => el.value);
 | 
			
		||||
          expect(text).withContext(`In ${browserName}`).toEqual(base);
 | 
			
		||||
 | 
			
		||||
          text = await page.$eval(getSelector("26R"), el => el.value);
 | 
			
		||||
          expect(text).withContext(`In ${browserName}`).toEqual(base);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("issue16473.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("issue16473.pdf", getAnnotationSelector("22R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must reset a formatted value after a change", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.type(getSelector("22R"), "a");
 | 
			
		||||
          await page.keyboard.press("Tab");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `${getQuerySelector("22R")}.value !== "Hello world"`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const text = await page.$eval(getSelector("22R"), el => el.value);
 | 
			
		||||
          expect(text).withContext(`In ${browserName}`).toEqual("aHello World");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("Link annotations with internal destinations", () => {
 | 
			
		||||
  describe("bug1708041.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "bug1708041.pdf",
 | 
			
		||||
        ".page[data-page-number='1'] .annotationLayer"
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must click on a link and check if it navigates to the correct page", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const pageOneSelector = ".page[data-page-number='1']";
 | 
			
		||||
          const linkSelector = `${pageOneSelector} #pdfjs_internal_id_42R`;
 | 
			
		||||
          await page.waitForSelector(linkSelector);
 | 
			
		||||
          const linkTitle = await page.$eval(linkSelector, el => el.title);
 | 
			
		||||
          expect(linkTitle)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual("Go to the last page");
 | 
			
		||||
          await page.click(linkSelector);
 | 
			
		||||
          const pageSixTextLayerSelector =
 | 
			
		||||
            ".page[data-page-number='6'] .textLayer";
 | 
			
		||||
          await page.waitForSelector(pageSixTextLayerSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            sel => {
 | 
			
		||||
              const textLayer = document.querySelector(sel);
 | 
			
		||||
              return document.activeElement === textLayer;
 | 
			
		||||
            },
 | 
			
		||||
            {},
 | 
			
		||||
            pageSixTextLayerSelector
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("Annotation and storage", () => {
 | 
			
		||||
  describe("issue14023.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("issue14023.pdf", getAnnotationSelector("64R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must let checkboxes with the same name behave like radio buttons", async () => {
 | 
			
		||||
      const text1 = "hello world!";
 | 
			
		||||
      const text2 = "!dlrow olleh";
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          // Text field.
 | 
			
		||||
          await page.type(getSelector("64R"), text1);
 | 
			
		||||
          // Checkbox.
 | 
			
		||||
          await page.click(getAnnotationSelector("65R"));
 | 
			
		||||
          // Radio.
 | 
			
		||||
          await page.click(getAnnotationSelector("67R"));
 | 
			
		||||
 | 
			
		||||
          for (const [pageNumber, textId, checkId, radio1Id, radio2Id] of [
 | 
			
		||||
            [2, "18R", "19R", "21R", "20R"],
 | 
			
		||||
            [5, "23R", "24R", "22R", "25R"],
 | 
			
		||||
          ]) {
 | 
			
		||||
            await page.evaluate(n => {
 | 
			
		||||
              window.document
 | 
			
		||||
                .querySelectorAll(`[data-page-number="${n}"][class="page"]`)[0]
 | 
			
		||||
                .scrollIntoView();
 | 
			
		||||
            }, pageNumber);
 | 
			
		||||
 | 
			
		||||
            // Need to wait to have a displayed text input.
 | 
			
		||||
            await page.waitForSelector(getSelector(textId), {
 | 
			
		||||
              timeout: 0,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const text = await page.$eval(getSelector(textId), el => el.value);
 | 
			
		||||
            expect(text).withContext(`In ${browserName}`).toEqual(text1);
 | 
			
		||||
 | 
			
		||||
            let checked = await page.$eval(
 | 
			
		||||
              getSelector(checkId),
 | 
			
		||||
              el => el.checked
 | 
			
		||||
            );
 | 
			
		||||
            expect(checked).toEqual(true);
 | 
			
		||||
 | 
			
		||||
            checked = await page.$eval(getSelector(radio1Id), el => el.checked);
 | 
			
		||||
            expect(checked).toEqual(false);
 | 
			
		||||
 | 
			
		||||
            checked = await page.$eval(getSelector(radio2Id), el => el.checked);
 | 
			
		||||
            expect(checked).toEqual(false);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          // Change data on page 5 and check that other pages changed.
 | 
			
		||||
          // Text field.
 | 
			
		||||
          await page.type(getSelector("23R"), text2);
 | 
			
		||||
          // Checkbox.
 | 
			
		||||
          await page.click(getAnnotationSelector("24R"));
 | 
			
		||||
          // Radio.
 | 
			
		||||
          await page.click(getAnnotationSelector("25R"));
 | 
			
		||||
 | 
			
		||||
          for (const [pageNumber, textId, checkId, radio1Id, radio2Id] of [
 | 
			
		||||
            [1, "64R", "65R", "67R", "68R"],
 | 
			
		||||
            [2, "18R", "19R", "21R", "20R"],
 | 
			
		||||
          ]) {
 | 
			
		||||
            await page.evaluate(n => {
 | 
			
		||||
              window.document
 | 
			
		||||
                .querySelectorAll(`[data-page-number="${n}"][class="page"]`)[0]
 | 
			
		||||
                .scrollIntoView();
 | 
			
		||||
            }, pageNumber);
 | 
			
		||||
 | 
			
		||||
            // Need to wait to have a displayed text input.
 | 
			
		||||
            await page.waitForSelector(getSelector(textId), {
 | 
			
		||||
              timeout: 0,
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            const text = await page.$eval(getSelector(textId), el => el.value);
 | 
			
		||||
            expect(text)
 | 
			
		||||
              .withContext(`In ${browserName}`)
 | 
			
		||||
              .toEqual(text2 + text1);
 | 
			
		||||
 | 
			
		||||
            let checked = await page.$eval(
 | 
			
		||||
              getSelector(checkId),
 | 
			
		||||
              el => el.checked
 | 
			
		||||
            );
 | 
			
		||||
            expect(checked).toEqual(false);
 | 
			
		||||
 | 
			
		||||
            checked = await page.$eval(getSelector(radio1Id), el => el.checked);
 | 
			
		||||
            expect(checked).toEqual(false);
 | 
			
		||||
 | 
			
		||||
            checked = await page.$eval(getSelector(radio2Id), el => el.checked);
 | 
			
		||||
            expect(checked).toEqual(false);
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
describe("ResetForm action", () => {
 | 
			
		||||
  describe("resetform.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("resetform.pdf", getAnnotationSelector("63R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must reset all fields", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const base = "hello world";
 | 
			
		||||
          for (let i = 63; i <= 67; i++) {
 | 
			
		||||
            await page.type(getSelector(`${i}R`), base);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const selectors = [69, 71, 75].map(n =>
 | 
			
		||||
            getAnnotationSelector(`${n}R`)
 | 
			
		||||
          );
 | 
			
		||||
          for (const selector of selectors) {
 | 
			
		||||
            await page.click(selector);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          await page.select(getSelector("78R"), "b");
 | 
			
		||||
          await page.select(getSelector("81R"), "f");
 | 
			
		||||
 | 
			
		||||
          await page.click(getAnnotationSelector("82R"));
 | 
			
		||||
          await page.waitForFunction(`${getQuerySelector("63R")}.value === ""`);
 | 
			
		||||
 | 
			
		||||
          for (let i = 63; i <= 68; i++) {
 | 
			
		||||
            const text = await page.$eval(getSelector(`${i}R`), el => el.value);
 | 
			
		||||
            expect(text).withContext(`In ${browserName}`).toEqual("");
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const ids = [69, 71, 72, 73, 74, 75, 76, 77];
 | 
			
		||||
          for (const id of ids) {
 | 
			
		||||
            const checked = await page.$eval(
 | 
			
		||||
              getSelector(`${id}R`),
 | 
			
		||||
              el => el.checked
 | 
			
		||||
            );
 | 
			
		||||
            expect(checked).withContext(`In ${browserName}`).toEqual(false);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          let selected = await page.$eval(
 | 
			
		||||
            `${getSelector("78R")} [value="a"]`,
 | 
			
		||||
            el => el.selected
 | 
			
		||||
          );
 | 
			
		||||
          expect(selected).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
 | 
			
		||||
          selected = await page.$eval(
 | 
			
		||||
            `${getSelector("81R")} [value="d"]`,
 | 
			
		||||
            el => el.selected
 | 
			
		||||
          );
 | 
			
		||||
          expect(selected).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must reset some fields", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const base = "hello world";
 | 
			
		||||
          for (let i = 63; i <= 68; i++) {
 | 
			
		||||
            await page.type(getSelector(`${i}R`), base);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          const selectors = [69, 71, 72, 73, 75].map(n =>
 | 
			
		||||
            getAnnotationSelector(`${n}R`)
 | 
			
		||||
          );
 | 
			
		||||
          for (const selector of selectors) {
 | 
			
		||||
            await page.click(selector);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          await page.select(getSelector("78R"), "b");
 | 
			
		||||
          await page.select(getSelector("81R"), "f");
 | 
			
		||||
 | 
			
		||||
          await page.click(getAnnotationSelector("84R"));
 | 
			
		||||
          await page.waitForFunction(`${getQuerySelector("63R")}.value === ""`);
 | 
			
		||||
 | 
			
		||||
          for (let i = 63; i <= 68; i++) {
 | 
			
		||||
            const expected = (i - 3) % 2 === 0 ? "" : base;
 | 
			
		||||
            const text = await page.$eval(getSelector(`${i}R`), el => el.value);
 | 
			
		||||
            expect(text).withContext(`In ${browserName}`).toEqual(expected);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          let ids = [69, 72, 73, 74, 76, 77];
 | 
			
		||||
          for (const id of ids) {
 | 
			
		||||
            const checked = await page.$eval(
 | 
			
		||||
              getSelector(`${id}R`),
 | 
			
		||||
              el => el.checked
 | 
			
		||||
            );
 | 
			
		||||
            expect(checked)
 | 
			
		||||
              .withContext(`In ${browserName + id}`)
 | 
			
		||||
              .toEqual(false);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          ids = [71, 75];
 | 
			
		||||
          for (const id of ids) {
 | 
			
		||||
            const checked = await page.$eval(
 | 
			
		||||
              getSelector(`${id}R`),
 | 
			
		||||
              el => el.checked
 | 
			
		||||
            );
 | 
			
		||||
            expect(checked).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          let selected = await page.$eval(
 | 
			
		||||
            `${getSelector("78R")} [value="a"]`,
 | 
			
		||||
            el => el.selected
 | 
			
		||||
          );
 | 
			
		||||
          expect(selected).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
 | 
			
		||||
          selected = await page.$eval(
 | 
			
		||||
            `${getSelector("81R")} [value="f"]`,
 | 
			
		||||
            el => el.selected
 | 
			
		||||
          );
 | 
			
		||||
          expect(selected).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("FreeText widget", () => {
 | 
			
		||||
    describe("issue14438.pdf", () => {
 | 
			
		||||
      let pages;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        pages = await loadAndWait(
 | 
			
		||||
          "issue14438.pdf",
 | 
			
		||||
          getAnnotationSelector("10R")
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closePages(pages);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the FreeText annotation has a popup", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const selector = getAnnotationSelector("10R");
 | 
			
		||||
            await page.click(selector);
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector}').hidden === false`
 | 
			
		||||
            );
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Ink widget and its popup after editing", () => {
 | 
			
		||||
    describe("annotation-caret-ink.pdf", () => {
 | 
			
		||||
      let pages;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        pages = await loadAndWait(
 | 
			
		||||
          "annotation-caret-ink.pdf",
 | 
			
		||||
          getAnnotationSelector("25R")
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closePages(pages);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the Ink annotation has a popup", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const selector = getAnnotationSelector("25R");
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector}').hidden === false`
 | 
			
		||||
            );
 | 
			
		||||
            await page.click("#editorFreeText");
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector}').hidden === true`
 | 
			
		||||
            );
 | 
			
		||||
            await page.click("#editorFreeText");
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector}').hidden === false`
 | 
			
		||||
            );
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Don't use AP when /NeedAppearances is true", () => {
 | 
			
		||||
    describe("bug1844583.pdf", () => {
 | 
			
		||||
      let pages;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        pages = await loadAndWait(
 | 
			
		||||
          "bug1844583.pdf",
 | 
			
		||||
          getAnnotationSelector("8R")
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closePages(pages);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check the content of the text field", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const text = await page.$eval(getSelector("8R"), el => el.value);
 | 
			
		||||
            expect(text)
 | 
			
		||||
              .withContext(`In ${browserName}`)
 | 
			
		||||
              .toEqual("Hello World");
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Toggle popup with keyboard", () => {
 | 
			
		||||
    describe("tagged_stamp.pdf", () => {
 | 
			
		||||
      let pages;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        pages = await loadAndWait(
 | 
			
		||||
          "tagged_stamp.pdf",
 | 
			
		||||
          getAnnotationSelector("20R")
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closePages(pages);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the popup has the correct visibility", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const selector = getAnnotationSelector("21R");
 | 
			
		||||
            let hidden = await page.$eval(selector, el => el.hidden);
 | 
			
		||||
            expect(hidden).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
 | 
			
		||||
            await page.focus(getAnnotationSelector("20R"));
 | 
			
		||||
            await page.keyboard.press("Enter");
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector}').hidden !== true`
 | 
			
		||||
            );
 | 
			
		||||
            hidden = await page.$eval(selector, el => el.hidden);
 | 
			
		||||
            expect(hidden).withContext(`In ${browserName}`).toEqual(false);
 | 
			
		||||
 | 
			
		||||
            await page.keyboard.press("Enter");
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector}').hidden !== false`
 | 
			
		||||
            );
 | 
			
		||||
            hidden = await page.$eval(selector, el => el.hidden);
 | 
			
		||||
            expect(hidden).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
 | 
			
		||||
            await page.keyboard.press("Enter");
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector}').hidden !== true`
 | 
			
		||||
            );
 | 
			
		||||
            hidden = await page.$eval(selector, el => el.hidden);
 | 
			
		||||
            expect(hidden).withContext(`In ${browserName}`).toEqual(false);
 | 
			
		||||
 | 
			
		||||
            await page.keyboard.press("Escape");
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              `document.querySelector('${selector}').hidden !== false`
 | 
			
		||||
            );
 | 
			
		||||
            hidden = await page.$eval(selector, el => el.hidden);
 | 
			
		||||
            expect(hidden).withContext(`In ${browserName}`).toEqual(true);
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Annotation with empty popup and aria", () => {
 | 
			
		||||
    describe("issue14438.pdf", () => {
 | 
			
		||||
      let pages;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        pages = await loadAndWait(
 | 
			
		||||
          "highlights.pdf",
 | 
			
		||||
          getAnnotationSelector("693R")
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closePages(pages);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the highlight annotation has no popup and no aria-haspopup attribute", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const highlightSelector = getAnnotationSelector("693R");
 | 
			
		||||
            const popupSelector = getAnnotationSelector("694R");
 | 
			
		||||
            await page.waitForFunction(
 | 
			
		||||
              // No aria-haspopup attribute,
 | 
			
		||||
              `document.querySelector('${highlightSelector}').ariaHasPopup === null ` +
 | 
			
		||||
                // and no popup.
 | 
			
		||||
                `&& document.querySelector('${popupSelector}') === null`
 | 
			
		||||
            );
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Rotated annotation and its clickable area", () => {
 | 
			
		||||
    describe("rotated_ink.pdf", () => {
 | 
			
		||||
      let pages;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        pages = await loadAndWait(
 | 
			
		||||
          "rotated_ink.pdf",
 | 
			
		||||
          getAnnotationSelector("18R")
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closePages(pages);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the clickable area has been rotated", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const rect = await getRect(page, getAnnotationSelector("18R"));
 | 
			
		||||
            const promisePopup = page.waitForSelector(
 | 
			
		||||
              getAnnotationSelector("19R"),
 | 
			
		||||
              { visible: true }
 | 
			
		||||
            );
 | 
			
		||||
            await page.mouse.move(
 | 
			
		||||
              rect.x + rect.width * 0.1,
 | 
			
		||||
              rect.y + rect.height * 0.9
 | 
			
		||||
            );
 | 
			
		||||
            await promisePopup;
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Text under some annotations", () => {
 | 
			
		||||
    describe("bug1885505.pdf", () => {
 | 
			
		||||
      let pages;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        pages = await loadAndWait(
 | 
			
		||||
          "bug1885505.pdf",
 | 
			
		||||
          ":is(" +
 | 
			
		||||
            [56, 58, 60, 65]
 | 
			
		||||
              .map(id => getAnnotationSelector(`${id}R`))
 | 
			
		||||
              .join(", ") +
 | 
			
		||||
            ")"
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closePages(pages);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the text under a highlight annotation exist in the DOM", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const text = await page.$eval(
 | 
			
		||||
              `${getAnnotationSelector("56R")} mark`,
 | 
			
		||||
              el => el.textContent
 | 
			
		||||
            );
 | 
			
		||||
            expect(text).withContext(`In ${browserName}`).toEqual("Languages");
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the text under an underline annotation exist in the DOM", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const text = await page.$eval(
 | 
			
		||||
              `${getAnnotationSelector("58R")} u`,
 | 
			
		||||
              el => el.textContent
 | 
			
		||||
            );
 | 
			
		||||
            expect(text).withContext(`In ${browserName}`).toEqual("machine");
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the text under a squiggly annotation exist in the DOM", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const text = await page.$eval(
 | 
			
		||||
              `${getAnnotationSelector("60R")} u`,
 | 
			
		||||
              el => el.textContent
 | 
			
		||||
            );
 | 
			
		||||
            expect(text).withContext(`In ${browserName}`)
 | 
			
		||||
              .toEqual(`paths through nested loops. We have implemented
 | 
			
		||||
a dynamic compiler for JavaScript based on our`);
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the text under a strikeout annotation exist in the DOM", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const text = await page.$eval(
 | 
			
		||||
              `${getAnnotationSelector("65R")} s`,
 | 
			
		||||
              el => el.textContent
 | 
			
		||||
            );
 | 
			
		||||
            expect(text)
 | 
			
		||||
              .withContext(`In ${browserName}`)
 | 
			
		||||
              .toEqual("Experimentation,");
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Annotation without popup and enableComment set to true", () => {
 | 
			
		||||
    describe("annotation-text-without-popup.pdf", () => {
 | 
			
		||||
      let pages;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        pages = await loadAndWait(
 | 
			
		||||
          "annotation-text-without-popup.pdf",
 | 
			
		||||
          getAnnotationSelector("4R"),
 | 
			
		||||
          "page-fit",
 | 
			
		||||
          null,
 | 
			
		||||
          { enableComment: true }
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closePages(pages);
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("must check that the popup is shown", async () => {
 | 
			
		||||
        await Promise.all(
 | 
			
		||||
          pages.map(async ([browserName, page]) => {
 | 
			
		||||
            const rect = await getRect(page, getAnnotationSelector("4R"));
 | 
			
		||||
 | 
			
		||||
            // Hover the annotation, the popup should be visible.
 | 
			
		||||
            let promisePopup = page.waitForSelector("#commentPopup", {
 | 
			
		||||
              visible: true,
 | 
			
		||||
            });
 | 
			
		||||
            await page.mouse.move(
 | 
			
		||||
              rect.x + rect.width / 2,
 | 
			
		||||
              rect.y + rect.height / 2
 | 
			
		||||
            );
 | 
			
		||||
            await promisePopup;
 | 
			
		||||
 | 
			
		||||
            // Move the mouse away, the popup should be hidden.
 | 
			
		||||
            promisePopup = page.waitForSelector("#commentPopup", {
 | 
			
		||||
              visible: false,
 | 
			
		||||
            });
 | 
			
		||||
            await page.mouse.move(
 | 
			
		||||
              rect.x - rect.width / 2,
 | 
			
		||||
              rect.y - rect.height / 2
 | 
			
		||||
            );
 | 
			
		||||
            await promisePopup;
 | 
			
		||||
 | 
			
		||||
            // Click the annotation, the popup should be visible.
 | 
			
		||||
            promisePopup = page.waitForSelector("#commentPopup", {
 | 
			
		||||
              visible: true,
 | 
			
		||||
            });
 | 
			
		||||
            await page.mouse.click(
 | 
			
		||||
              rect.x + rect.width / 2,
 | 
			
		||||
              rect.y + rect.height / 2
 | 
			
		||||
            );
 | 
			
		||||
            await promisePopup;
 | 
			
		||||
 | 
			
		||||
            // Click again, the popup should be hidden.
 | 
			
		||||
            promisePopup = page.waitForSelector("#commentPopup", {
 | 
			
		||||
              visible: false,
 | 
			
		||||
            });
 | 
			
		||||
            await page.mouse.click(
 | 
			
		||||
              rect.x + rect.width / 2,
 | 
			
		||||
              rect.y + rect.height / 2
 | 
			
		||||
            );
 | 
			
		||||
            await promisePopup;
 | 
			
		||||
          })
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										262
									
								
								test/integration/autolinker_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								test/integration/autolinker_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,262 @@
 | 
			
		||||
/* Copyright 2025 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 {
 | 
			
		||||
  awaitPromise,
 | 
			
		||||
  closePages,
 | 
			
		||||
  createPromise,
 | 
			
		||||
  loadAndWait,
 | 
			
		||||
} from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
function waitForLinkAnnotations(page, pageNumber) {
 | 
			
		||||
  return page.evaluateHandle(
 | 
			
		||||
    number => [
 | 
			
		||||
      new Promise(resolve => {
 | 
			
		||||
        const { eventBus } = window.PDFViewerApplication;
 | 
			
		||||
        eventBus.on("linkannotationsadded", function listener(e) {
 | 
			
		||||
          if (number === undefined || e.pageNumber === number) {
 | 
			
		||||
            resolve();
 | 
			
		||||
            eventBus.off("linkannotationsadded", listener);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }),
 | 
			
		||||
    ],
 | 
			
		||||
    pageNumber
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function recordInitialLinkAnnotationsEvent(eventBus) {
 | 
			
		||||
  globalThis.initialLinkAnnotationsEventFired = false;
 | 
			
		||||
  eventBus.on(
 | 
			
		||||
    "linkannotationsadded",
 | 
			
		||||
    () => {
 | 
			
		||||
      globalThis.initialLinkAnnotationsEventFired = true;
 | 
			
		||||
    },
 | 
			
		||||
    { once: true }
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
function waitForInitialLinkAnnotations(page) {
 | 
			
		||||
  return createPromise(page, resolve => {
 | 
			
		||||
    if (globalThis.initialLinkAnnotationsEventFired) {
 | 
			
		||||
      resolve();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    window.PDFViewerApplication.eventBus.on("linkannotationsadded", resolve, {
 | 
			
		||||
      once: true,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe("autolinker", function () {
 | 
			
		||||
  describe("bug1019475_2.pdf", function () {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "bug1019475_2.pdf",
 | 
			
		||||
        ".annotationLayer",
 | 
			
		||||
        null,
 | 
			
		||||
        null,
 | 
			
		||||
        {
 | 
			
		||||
          enableAutoLinking: true,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must appropriately add link annotations when relevant", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await waitForLinkAnnotations(page);
 | 
			
		||||
          const url = await page.$$eval(
 | 
			
		||||
            ".annotationLayer > .linkAnnotation > a",
 | 
			
		||||
            annotations => annotations.map(a => a.href)
 | 
			
		||||
          );
 | 
			
		||||
          expect(url.length).withContext(`In ${browserName}`).toEqual(1);
 | 
			
		||||
          expect(url[0])
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual("http://www.mozilla.org/");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("bug1019475_1.pdf", function () {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "bug1019475_1.pdf",
 | 
			
		||||
        ".annotationLayer",
 | 
			
		||||
        null,
 | 
			
		||||
        null,
 | 
			
		||||
        {
 | 
			
		||||
          enableAutoLinking: true,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must not add links when unnecessary", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await waitForLinkAnnotations(page);
 | 
			
		||||
          const linkIds = await page.$$eval(
 | 
			
		||||
            ".annotationLayer > .linkAnnotation > a",
 | 
			
		||||
            annotations =>
 | 
			
		||||
              annotations.map(a => a.getAttribute("data-element-id"))
 | 
			
		||||
          );
 | 
			
		||||
          expect(linkIds.length).withContext(`In ${browserName}`).toEqual(3);
 | 
			
		||||
          linkIds.forEach(id =>
 | 
			
		||||
            expect(id)
 | 
			
		||||
              .withContext(`In ${browserName}`)
 | 
			
		||||
              .not.toContain("inferred_link_")
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("pr19449.pdf", function () {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("pr19449.pdf", ".annotationLayer", null, null, {
 | 
			
		||||
        docBaseUrl: "http://example.com",
 | 
			
		||||
        enableAutoLinking: true,
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must not add links that overlap even if the URLs are different", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await waitForLinkAnnotations(page);
 | 
			
		||||
          const linkIds = await page.$$eval(
 | 
			
		||||
            ".annotationLayer > .linkAnnotation > a",
 | 
			
		||||
            annotations =>
 | 
			
		||||
              annotations.map(a => a.getAttribute("data-element-id"))
 | 
			
		||||
          );
 | 
			
		||||
          expect(linkIds.length).withContext(`In ${browserName}`).toEqual(1);
 | 
			
		||||
          linkIds.forEach(id =>
 | 
			
		||||
            expect(id)
 | 
			
		||||
              .withContext(`In ${browserName}`)
 | 
			
		||||
              .not.toContain("inferred_link_")
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("PR 19470", function () {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "bug1019475_2.pdf",
 | 
			
		||||
        ".annotationLayer",
 | 
			
		||||
        null,
 | 
			
		||||
        null,
 | 
			
		||||
        {
 | 
			
		||||
          enableAutoLinking: true,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must not repeatedly add link annotations redundantly", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await waitForLinkAnnotations(page);
 | 
			
		||||
          let url = await page.$$eval(
 | 
			
		||||
            ".annotationLayer > .linkAnnotation > a",
 | 
			
		||||
            annotations => annotations.map(a => a.href)
 | 
			
		||||
          );
 | 
			
		||||
          expect(url.length).withContext(`In ${browserName}`).toEqual(1);
 | 
			
		||||
 | 
			
		||||
          await page.evaluate(() =>
 | 
			
		||||
            window.PDFViewerApplication.pdfViewer.updateScale({
 | 
			
		||||
              drawingDelay: -1,
 | 
			
		||||
              scaleFactor: 2,
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
          await waitForLinkAnnotations(page);
 | 
			
		||||
          url = await page.$$eval(
 | 
			
		||||
            ".annotationLayer > .linkAnnotation > a",
 | 
			
		||||
            annotations => annotations.map(a => a.href)
 | 
			
		||||
          );
 | 
			
		||||
          expect(url.length).withContext(`In ${browserName}`).toEqual(1);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("when highlighting search results", function () {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "issue3115r.pdf",
 | 
			
		||||
        ".annotationLayer",
 | 
			
		||||
        null,
 | 
			
		||||
        { eventBusSetup: recordInitialLinkAnnotationsEvent },
 | 
			
		||||
        { enableAutoLinking: true }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must find links that overlap with search results", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await awaitPromise(await waitForInitialLinkAnnotations(page));
 | 
			
		||||
 | 
			
		||||
          const linkAnnotationsPromise = await waitForLinkAnnotations(page, 36);
 | 
			
		||||
 | 
			
		||||
          // Search for "rich.edu"
 | 
			
		||||
          await page.click("#viewFindButton");
 | 
			
		||||
          await page.waitForSelector("#viewFindButton", { hidden: false });
 | 
			
		||||
          await page.type("#findInput", "rich.edu");
 | 
			
		||||
          await page.waitForSelector(".textLayer .highlight");
 | 
			
		||||
 | 
			
		||||
          await awaitPromise(linkAnnotationsPromise);
 | 
			
		||||
 | 
			
		||||
          const urls = await page.$$eval(
 | 
			
		||||
            ".page[data-page-number='36'] > .annotationLayer > .linkAnnotation > a",
 | 
			
		||||
            annotations => annotations.map(a => a.href)
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          expect(urls)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toContain(jasmine.stringContaining("rich.edu"));
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										97
									
								
								test/integration/caret_browsing_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								test/integration/caret_browsing_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
/* Copyright 2021 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 { closePages, getRect, loadAndWait } from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
const waitForSelectionChange = (page, selection) =>
 | 
			
		||||
  page.waitForFunction(
 | 
			
		||||
    // We need to replace EOL on Windows to make the test pass.
 | 
			
		||||
    sel => document.getSelection().toString().replaceAll("\r\n", "\n") === sel,
 | 
			
		||||
    {},
 | 
			
		||||
    selection
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
describe("Caret browsing", () => {
 | 
			
		||||
  describe("Selection", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("tracemonkey.pdf", ".textLayer .endOfContent");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must move the caret down and check the selection", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const spanRect = await getRect(
 | 
			
		||||
            page,
 | 
			
		||||
            `.page[data-page-number="1"] > .textLayer > span`
 | 
			
		||||
          );
 | 
			
		||||
          await page.mouse.click(
 | 
			
		||||
            spanRect.x + 1,
 | 
			
		||||
            spanRect.y + spanRect.height / 2,
 | 
			
		||||
            { count: 2 }
 | 
			
		||||
          );
 | 
			
		||||
          await page.keyboard.down("Shift");
 | 
			
		||||
          for (let i = 0; i < 6; i++) {
 | 
			
		||||
            await page.keyboard.press("ArrowRight");
 | 
			
		||||
          }
 | 
			
		||||
          await page.keyboard.up("Shift");
 | 
			
		||||
          await waitForSelectionChange(page, "Trace-based");
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Shift");
 | 
			
		||||
          await page.keyboard.press("ArrowDown");
 | 
			
		||||
          await page.keyboard.up("Shift");
 | 
			
		||||
 | 
			
		||||
          // The caret is just before Languages.
 | 
			
		||||
          await waitForSelectionChange(
 | 
			
		||||
            page,
 | 
			
		||||
            "Trace-based Just-in-Time Type Specialization for Dynamic\n"
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Shift");
 | 
			
		||||
          await page.keyboard.press("ArrowDown");
 | 
			
		||||
          await page.keyboard.up("Shift");
 | 
			
		||||
 | 
			
		||||
          // The caret is just before Mike Shaver.
 | 
			
		||||
          await waitForSelectionChange(
 | 
			
		||||
            page,
 | 
			
		||||
            "Trace-based Just-in-Time Type Specialization for Dynamic\nLanguages\nAndreas Gal∗+, Brendan Eich∗, "
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Shift");
 | 
			
		||||
          await page.keyboard.press("ArrowUp");
 | 
			
		||||
          await page.keyboard.up("Shift");
 | 
			
		||||
 | 
			
		||||
          // The caret is just before Languages.
 | 
			
		||||
          await waitForSelectionChange(
 | 
			
		||||
            page,
 | 
			
		||||
            "Trace-based Just-in-Time Type Specialization for Dynamic\n"
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.keyboard.down("Shift");
 | 
			
		||||
          await page.keyboard.press("ArrowUp");
 | 
			
		||||
          await page.keyboard.up("Shift");
 | 
			
		||||
 | 
			
		||||
          // The caret is in the middle of Time.
 | 
			
		||||
          await waitForSelectionChange(page, "Trace-based Just-in-Tim");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										627
									
								
								test/integration/comment_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										627
									
								
								test/integration/comment_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,627 @@
 | 
			
		||||
/* Copyright 2025 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 {
 | 
			
		||||
  awaitPromise,
 | 
			
		||||
  closePages,
 | 
			
		||||
  createPromise,
 | 
			
		||||
  dragAndDrop,
 | 
			
		||||
  getEditorSelector,
 | 
			
		||||
  getRect,
 | 
			
		||||
  getSpanRectFromText,
 | 
			
		||||
  loadAndWait,
 | 
			
		||||
  scrollIntoView,
 | 
			
		||||
  selectEditor,
 | 
			
		||||
  switchToEditor,
 | 
			
		||||
  waitAndClick,
 | 
			
		||||
  waitForSerialized,
 | 
			
		||||
} from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
const switchToHighlight = switchToEditor.bind(null, "Highlight");
 | 
			
		||||
const switchToStamp = switchToEditor.bind(null, "Stamp");
 | 
			
		||||
const switchToComment = switchToEditor.bind(null, "Comment");
 | 
			
		||||
 | 
			
		||||
const highlightSpan = async (page, pageIndex, text) => {
 | 
			
		||||
  const rect = await getSpanRectFromText(page, pageIndex, text);
 | 
			
		||||
  const x = rect.x + rect.width / 2;
 | 
			
		||||
  const y = rect.y + rect.height / 2;
 | 
			
		||||
  await page.mouse.click(x, y, { count: 2, delay: 100 });
 | 
			
		||||
  await page.waitForSelector(getEditorSelector(0));
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const editComment = async (page, editorSelector, comment) => {
 | 
			
		||||
  const commentButtonSelector = `${editorSelector} button.comment`;
 | 
			
		||||
  await waitAndClick(page, commentButtonSelector);
 | 
			
		||||
 | 
			
		||||
  const textInputSelector = "#commentManagerTextInput";
 | 
			
		||||
  await page.waitForSelector(textInputSelector, {
 | 
			
		||||
    visible: true,
 | 
			
		||||
  });
 | 
			
		||||
  await page.type(textInputSelector, comment);
 | 
			
		||||
  await waitAndClick(page, "#commentManagerSaveButton");
 | 
			
		||||
  await page.waitForSelector("#commentManagerDialog", {
 | 
			
		||||
    visible: false,
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe("Comment", () => {
 | 
			
		||||
  describe("Comment edit dialog must be visible in ltr", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "bug1989304.pdf",
 | 
			
		||||
        ".annotationEditorLayer",
 | 
			
		||||
        "page-width",
 | 
			
		||||
        null,
 | 
			
		||||
        { enableComment: true }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must set the comment dialog in the viewport (LTR)", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToHighlight(page);
 | 
			
		||||
 | 
			
		||||
          await scrollIntoView(page, ".textLayer span:last-of-type");
 | 
			
		||||
          const rect = await getSpanRectFromText(page, 1, "...");
 | 
			
		||||
          const x = rect.x + rect.width / 2;
 | 
			
		||||
          const y = rect.y + rect.height / 2;
 | 
			
		||||
          // Here and elsewhere, we add a small delay between press and release
 | 
			
		||||
          // to make sure that a pointerup event is triggered after
 | 
			
		||||
          // selectionchange.
 | 
			
		||||
          // It works with a value of 1ms, but we use 100ms to be sure.
 | 
			
		||||
          await page.mouse.click(x, y, { count: 2, delay: 100 });
 | 
			
		||||
          await page.waitForSelector(getEditorSelector(0));
 | 
			
		||||
 | 
			
		||||
          const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
 | 
			
		||||
          await waitAndClick(page, commentButtonSelector);
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#commentManagerDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          const dialogRect = await getRect(page, "#commentManagerDialog");
 | 
			
		||||
          const viewport = await page.evaluate(() => ({
 | 
			
		||||
            width: document.documentElement.clientWidth,
 | 
			
		||||
            height: document.documentElement.clientHeight,
 | 
			
		||||
          }));
 | 
			
		||||
          expect(dialogRect.x + dialogRect.width)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeLessThanOrEqual(viewport.width + 1);
 | 
			
		||||
          expect(dialogRect.y + dialogRect.height)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeLessThanOrEqual(viewport.height + 1);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Comment edit dialog must be visible in rtl", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "bug1989304.pdf",
 | 
			
		||||
        ".annotationEditorLayer",
 | 
			
		||||
        "page-width",
 | 
			
		||||
        null,
 | 
			
		||||
        { enableComment: true, localeProperties: "ar" }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must set the comment dialog in the viewport (RTL)", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToHighlight(page);
 | 
			
		||||
 | 
			
		||||
          await scrollIntoView(page, ".textLayer span:nth-of-type(4)");
 | 
			
		||||
          const rect = await getSpanRectFromText(page, 1, "World");
 | 
			
		||||
          const x = rect.x + rect.width / 2;
 | 
			
		||||
          const y = rect.y + rect.height / 2;
 | 
			
		||||
          await page.mouse.click(x, y, { count: 2, delay: 100 });
 | 
			
		||||
          await page.waitForSelector(getEditorSelector(0));
 | 
			
		||||
 | 
			
		||||
          const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
 | 
			
		||||
          await waitAndClick(page, commentButtonSelector);
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#commentManagerDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          const dialogRect = await getRect(page, "#commentManagerDialog");
 | 
			
		||||
          const viewport = await page.evaluate(() => ({
 | 
			
		||||
            height: window.innerHeight,
 | 
			
		||||
          }));
 | 
			
		||||
          expect(dialogRect.x + dialogRect.width)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeGreaterThanOrEqual(-1);
 | 
			
		||||
          expect(dialogRect.y + dialogRect.height)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeLessThanOrEqual(viewport.height + 1);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Update comment position and color in reading mode", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "comments.pdf",
 | 
			
		||||
        ".annotationEditorLayer",
 | 
			
		||||
        "page-fit",
 | 
			
		||||
        null,
 | 
			
		||||
        {
 | 
			
		||||
          enableComment: true,
 | 
			
		||||
          highlightEditorColors:
 | 
			
		||||
            "yellow=#FFFF00,green=#00FF00,blue=#0000FF,pink=#FF00FF,red=#FF0000",
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must set the comment button at the right place", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToStamp(page);
 | 
			
		||||
 | 
			
		||||
          const stampSelector = getEditorSelector(8);
 | 
			
		||||
          await selectEditor(page, stampSelector);
 | 
			
		||||
          await dragAndDrop(page, stampSelector, [[100, 100]]);
 | 
			
		||||
          await waitForSerialized(page, 1);
 | 
			
		||||
          const rectCommentButton = await getRect(
 | 
			
		||||
            page,
 | 
			
		||||
            `${stampSelector} .annotationCommentButton`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await switchToStamp(page, /* disable = */ true);
 | 
			
		||||
          const rectCommentButtonAfter = await getRect(
 | 
			
		||||
            page,
 | 
			
		||||
            `#pdfjs_internal_id_713R + .annotationCommentButton`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          expect(Math.abs(rectCommentButtonAfter.x - rectCommentButton.x))
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeLessThanOrEqual(1);
 | 
			
		||||
          expect(Math.abs(rectCommentButtonAfter.y - rectCommentButton.y))
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeLessThanOrEqual(1);
 | 
			
		||||
          expect(
 | 
			
		||||
            Math.abs(rectCommentButtonAfter.width - rectCommentButton.width)
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeLessThanOrEqual(1);
 | 
			
		||||
          expect(
 | 
			
		||||
            Math.abs(rectCommentButtonAfter.height - rectCommentButton.height)
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toBeLessThanOrEqual(1);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must set the right color to the comment button", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToHighlight(page);
 | 
			
		||||
 | 
			
		||||
          const highlightSelector = getEditorSelector(0);
 | 
			
		||||
          await selectEditor(page, highlightSelector);
 | 
			
		||||
          const colorButtonSelector = `${highlightSelector} .editToolbar button`;
 | 
			
		||||
          await page.waitForSelector(`${colorButtonSelector}.colorPicker`);
 | 
			
		||||
          await page.click(`${colorButtonSelector}.colorPicker`);
 | 
			
		||||
          await page.waitForSelector(`${colorButtonSelector}[title = "Red"]`);
 | 
			
		||||
          await page.click(`${colorButtonSelector}[title = "Red"]`);
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `.page[data-page-number = "1"] svg.highlight[fill = "#FF0000"]`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const commentButtonColor = await page.evaluate(selector => {
 | 
			
		||||
            const button = document.querySelector(
 | 
			
		||||
              `${selector} .annotationCommentButton`
 | 
			
		||||
            );
 | 
			
		||||
            return window.getComputedStyle(button).backgroundColor;
 | 
			
		||||
          }, highlightSelector);
 | 
			
		||||
 | 
			
		||||
          await switchToHighlight(page, /* disable = */ true);
 | 
			
		||||
 | 
			
		||||
          const commentButtonColorAfter = await page.evaluate(() => {
 | 
			
		||||
            const button = document.querySelector(
 | 
			
		||||
              "section[data-annotation-id='612R'] + .annotationCommentButton"
 | 
			
		||||
            );
 | 
			
		||||
            return window.getComputedStyle(button).backgroundColor;
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          expect(commentButtonColorAfter)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(commentButtonColor);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Comment buttons", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "tracemonkey.pdf",
 | 
			
		||||
        ".annotationEditorLayer",
 | 
			
		||||
        "page-width",
 | 
			
		||||
        null,
 | 
			
		||||
        { enableComment: true }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the comment button has a title", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToHighlight(page);
 | 
			
		||||
 | 
			
		||||
          const rect = await getSpanRectFromText(page, 1, "Languages");
 | 
			
		||||
          const x = rect.x + rect.width / 2;
 | 
			
		||||
          const y = rect.y + rect.height / 2;
 | 
			
		||||
          await page.mouse.click(x, y, { count: 2, delay: 100 });
 | 
			
		||||
          await page.waitForSelector(getEditorSelector(0));
 | 
			
		||||
 | 
			
		||||
          let commentButtonSelector = `${getEditorSelector(0)} button.comment`;
 | 
			
		||||
          await page.waitForSelector(commentButtonSelector, { visible: true });
 | 
			
		||||
          let title = await page.evaluate(
 | 
			
		||||
            selector => document.querySelector(selector).title,
 | 
			
		||||
            commentButtonSelector
 | 
			
		||||
          );
 | 
			
		||||
          expect(title)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual("Edit comment");
 | 
			
		||||
          await page.click(commentButtonSelector);
 | 
			
		||||
 | 
			
		||||
          const textInputSelector = "#commentManagerTextInput";
 | 
			
		||||
          await page.waitForSelector(textInputSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.type(textInputSelector, "Hello world!");
 | 
			
		||||
 | 
			
		||||
          await page.click("#commentManagerSaveButton");
 | 
			
		||||
 | 
			
		||||
          commentButtonSelector = `${getEditorSelector(0)} button.annotationCommentButton`;
 | 
			
		||||
          await page.waitForSelector(commentButtonSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          title = await page.evaluate(selector => {
 | 
			
		||||
            const button = document.querySelector(selector);
 | 
			
		||||
            return button.title;
 | 
			
		||||
          }, commentButtonSelector);
 | 
			
		||||
          expect(title)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual("Show comment");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the comment button is added in the annotation layer", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToHighlight(page);
 | 
			
		||||
 | 
			
		||||
          const rect = await getSpanRectFromText(page, 1, "Abstract");
 | 
			
		||||
          const x = rect.x + rect.width / 2;
 | 
			
		||||
          const y = rect.y + rect.height / 2;
 | 
			
		||||
          await page.mouse.click(x, y, { count: 2, delay: 100 });
 | 
			
		||||
          await page.waitForSelector(getEditorSelector(0));
 | 
			
		||||
 | 
			
		||||
          const comment = "Hello world!";
 | 
			
		||||
          await editComment(page, getEditorSelector(0), comment);
 | 
			
		||||
          await page.hover("#editorHighlightButton");
 | 
			
		||||
          let buttonSelector =
 | 
			
		||||
            ".annotationEditorLayer .annotationCommentButton";
 | 
			
		||||
          await page.waitForSelector(buttonSelector, { visible: true });
 | 
			
		||||
          await page.hover(buttonSelector);
 | 
			
		||||
          const popupSelector = "#commentPopup";
 | 
			
		||||
          await page.waitForSelector(popupSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          let popupText = await page.evaluate(
 | 
			
		||||
            selector => document.querySelector(selector).textContent,
 | 
			
		||||
            `${popupSelector} .commentPopupText`
 | 
			
		||||
          );
 | 
			
		||||
          expect(popupText).withContext(`In ${browserName}`).toEqual(comment);
 | 
			
		||||
 | 
			
		||||
          await page.hover("#editorHighlightButton");
 | 
			
		||||
          await switchToHighlight(page, /* disable = */ true);
 | 
			
		||||
 | 
			
		||||
          buttonSelector = ".annotationLayer .annotationCommentButton";
 | 
			
		||||
          await page.waitForSelector(buttonSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.hover(buttonSelector);
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector(popupSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          popupText = await page.evaluate(
 | 
			
		||||
            selector => document.querySelector(selector).textContent,
 | 
			
		||||
            `${popupSelector} .commentPopupText`
 | 
			
		||||
          );
 | 
			
		||||
          expect(popupText).withContext(`In ${browserName}`).toEqual(comment);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Focused element after editing", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "tracemonkey.pdf",
 | 
			
		||||
        ".annotationEditorLayer",
 | 
			
		||||
        "page-width",
 | 
			
		||||
        null,
 | 
			
		||||
        { enableComment: true }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the focus is moved on the comment button", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToHighlight(page);
 | 
			
		||||
 | 
			
		||||
          const rect = await getSpanRectFromText(page, 1, "Languages");
 | 
			
		||||
          const x = rect.x + rect.width / 2;
 | 
			
		||||
          const y = rect.y + rect.height / 2;
 | 
			
		||||
          await page.mouse.click(x, y, { count: 2, delay: 100 });
 | 
			
		||||
          await page.waitForSelector(getEditorSelector(0));
 | 
			
		||||
 | 
			
		||||
          const commentButtonSelector = `${getEditorSelector(0)} button.comment`;
 | 
			
		||||
          await waitAndClick(page, commentButtonSelector);
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#commentManagerCancelButton", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          const handle = await createPromise(page, resolve => {
 | 
			
		||||
            document
 | 
			
		||||
              .querySelector("button.comment")
 | 
			
		||||
              .addEventListener("focus", resolve, { once: true });
 | 
			
		||||
          });
 | 
			
		||||
          await page.click("#commentManagerCancelButton");
 | 
			
		||||
          await awaitPromise(handle);
 | 
			
		||||
 | 
			
		||||
          await waitAndClick(page, commentButtonSelector);
 | 
			
		||||
 | 
			
		||||
          const textInputSelector = "#commentManagerTextInput";
 | 
			
		||||
          await page.waitForSelector(textInputSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.type(textInputSelector, "Hello world!");
 | 
			
		||||
 | 
			
		||||
          await page.click("#commentManagerSaveButton");
 | 
			
		||||
          await page.waitForSelector("button.annotationCommentButton", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            sel => document.activeElement === document.querySelector(sel),
 | 
			
		||||
            {},
 | 
			
		||||
            "button.annotationCommentButton"
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Focused element after editing in reading mode", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "comments.pdf",
 | 
			
		||||
        ".annotationLayer",
 | 
			
		||||
        "page-width",
 | 
			
		||||
        null,
 | 
			
		||||
        { enableComment: true }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the focus is moved on the comment button", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const commentButtonSelector = `[data-annotation-id="612R"] + button.annotationCommentButton`;
 | 
			
		||||
          await waitAndClick(page, commentButtonSelector);
 | 
			
		||||
          const commentPopupSelector = "#commentPopup";
 | 
			
		||||
          const editButtonSelector = `${commentPopupSelector} button.commentPopupEdit`;
 | 
			
		||||
          await waitAndClick(page, editButtonSelector);
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#commentManagerCancelButton", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          let handle = await createPromise(page, resolve => {
 | 
			
		||||
            document
 | 
			
		||||
              .querySelector(
 | 
			
		||||
                `[data-annotation-id="612R"] + button.annotationCommentButton`
 | 
			
		||||
              )
 | 
			
		||||
              .addEventListener("focus", resolve, { once: true });
 | 
			
		||||
          });
 | 
			
		||||
          await page.click("#commentManagerCancelButton");
 | 
			
		||||
          await awaitPromise(handle);
 | 
			
		||||
 | 
			
		||||
          await waitAndClick(page, commentButtonSelector);
 | 
			
		||||
          await waitAndClick(page, editButtonSelector);
 | 
			
		||||
 | 
			
		||||
          const textInputSelector = "#commentManagerTextInput";
 | 
			
		||||
          await page.waitForSelector(textInputSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.type(textInputSelector, "Hello world!");
 | 
			
		||||
 | 
			
		||||
          handle = await createPromise(page, resolve => {
 | 
			
		||||
            document
 | 
			
		||||
              .querySelector(
 | 
			
		||||
                `[data-annotation-id="612R"] + button.annotationCommentButton`
 | 
			
		||||
              )
 | 
			
		||||
              .addEventListener("focus", resolve, { once: true });
 | 
			
		||||
          });
 | 
			
		||||
          await page.click("#commentManagerSaveButton");
 | 
			
		||||
          await awaitPromise(handle);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Comment sidebar", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "comments.pdf",
 | 
			
		||||
        ".annotationEditorLayer",
 | 
			
		||||
        "page-width",
 | 
			
		||||
        null,
 | 
			
		||||
        { enableComment: true }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the comment sidebar is resizable", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToComment(page);
 | 
			
		||||
 | 
			
		||||
          const sidebarSelector = "#editorCommentParamsToolbar";
 | 
			
		||||
          for (const extraWidth of [100, -100]) {
 | 
			
		||||
            const rect = await getRect(page, sidebarSelector);
 | 
			
		||||
            const resizerRect = await getRect(
 | 
			
		||||
              page,
 | 
			
		||||
              "#editorCommentsSidebarResizer"
 | 
			
		||||
            );
 | 
			
		||||
            const startX = resizerRect.x + resizerRect.width / 2;
 | 
			
		||||
            const startY = resizerRect.y + 2;
 | 
			
		||||
            await page.mouse.move(startX, startY);
 | 
			
		||||
            await page.mouse.down();
 | 
			
		||||
 | 
			
		||||
            const steps = 20;
 | 
			
		||||
            await page.mouse.move(startX - extraWidth, startY, { steps });
 | 
			
		||||
            await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
            const rectAfter = await getRect(page, sidebarSelector);
 | 
			
		||||
            expect(Math.abs(rectAfter.width - (rect.width + extraWidth)))
 | 
			
		||||
              .withContext(`In ${browserName}`)
 | 
			
		||||
              .toBeLessThanOrEqual(1);
 | 
			
		||||
            expect(Math.abs(rectAfter.x - (rect.x - extraWidth)))
 | 
			
		||||
              .withContext(`In ${browserName}`)
 | 
			
		||||
              .toBeLessThanOrEqual(1);
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that comments are in chronological order", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToComment(page);
 | 
			
		||||
 | 
			
		||||
          const checkDates = async () => {
 | 
			
		||||
            const dates = await page.evaluate(() =>
 | 
			
		||||
              Array.from(
 | 
			
		||||
                document.querySelectorAll(
 | 
			
		||||
                  `#editorCommentParamsToolbar ul > li > time`
 | 
			
		||||
                )
 | 
			
		||||
              ).map(time => new Date(time.getAttribute("datetime")))
 | 
			
		||||
            );
 | 
			
		||||
            for (let i = 0; i < dates.length - 1; i++) {
 | 
			
		||||
              expect(dates[i])
 | 
			
		||||
                .withContext(`In ${browserName}`)
 | 
			
		||||
                .toBeGreaterThanOrEqual(dates[i + 1]);
 | 
			
		||||
            }
 | 
			
		||||
          };
 | 
			
		||||
          await checkDates();
 | 
			
		||||
 | 
			
		||||
          // Add an highlight with a comment and check the order again.
 | 
			
		||||
          await switchToHighlight(page);
 | 
			
		||||
          await highlightSpan(page, 1, "Languages");
 | 
			
		||||
          const editorSelector = getEditorSelector(9);
 | 
			
		||||
          await page.waitForSelector(editorSelector);
 | 
			
		||||
          const commentButtonSelector = `${editorSelector} button.comment`;
 | 
			
		||||
          await waitAndClick(page, commentButtonSelector);
 | 
			
		||||
 | 
			
		||||
          const textInputSelector = "#commentManagerTextInput";
 | 
			
		||||
          await page.waitForSelector(textInputSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.type(textInputSelector, "Hello world!");
 | 
			
		||||
          await page.click("#commentManagerSaveButton");
 | 
			
		||||
          await waitForSerialized(page, 1);
 | 
			
		||||
 | 
			
		||||
          await switchToComment(page);
 | 
			
		||||
          await checkDates();
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that comments can be selected/unselected", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToComment(page);
 | 
			
		||||
 | 
			
		||||
          const firstElementSelector =
 | 
			
		||||
            "#editorCommentsSidebarList li:first-child";
 | 
			
		||||
          await waitAndClick(page, firstElementSelector);
 | 
			
		||||
          const popupSelector = "#commentPopup";
 | 
			
		||||
          await page.waitForSelector(popupSelector, { visible: true });
 | 
			
		||||
          const popupTextSelector = `${popupSelector} .commentPopupText`;
 | 
			
		||||
          await page.waitForSelector(popupTextSelector, {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          const popupText = await page.evaluate(
 | 
			
		||||
            selector => document.querySelector(selector).textContent,
 | 
			
		||||
            popupTextSelector
 | 
			
		||||
          );
 | 
			
		||||
          expect(popupText)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual("ABCDEFGHIJKLMNOPQRSTUVWXYZ");
 | 
			
		||||
 | 
			
		||||
          // Click again to unselect the comment.
 | 
			
		||||
          await waitAndClick(page, firstElementSelector);
 | 
			
		||||
          await page.waitForSelector(popupSelector, { visible: false });
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										181
									
								
								test/integration/copy_paste_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										181
									
								
								test/integration/copy_paste_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,181 @@
 | 
			
		||||
/* Copyright 2023 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 {
 | 
			
		||||
  closePages,
 | 
			
		||||
  copy,
 | 
			
		||||
  kbSelectAll,
 | 
			
		||||
  loadAndWait,
 | 
			
		||||
  mockClipboard,
 | 
			
		||||
  waitForEvent,
 | 
			
		||||
} from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
const selectAll = async page => {
 | 
			
		||||
  await waitForEvent({
 | 
			
		||||
    page,
 | 
			
		||||
    eventName: "selectionchange",
 | 
			
		||||
    action: () => kbSelectAll(page),
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  await page.waitForFunction(() => {
 | 
			
		||||
    const selection = document.getSelection();
 | 
			
		||||
    const hiddenCopyElement = document.getElementById("hiddenCopyElement");
 | 
			
		||||
    return selection.containsNode(hiddenCopyElement);
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
describe("Copy and paste", () => {
 | 
			
		||||
  describe("all text", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("tracemonkey.pdf", "#hiddenCopyElement", 100);
 | 
			
		||||
      await mockClipboard(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that we've all the contents on copy/paste", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            ".page[data-page-number='1'] .textLayer .endOfContent"
 | 
			
		||||
          );
 | 
			
		||||
          await selectAll(page);
 | 
			
		||||
 | 
			
		||||
          await copy(page);
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector('#viewerContainer').style.cursor !== "wait"`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            async () =>
 | 
			
		||||
              !!(await navigator.clipboard.readText())?.includes(
 | 
			
		||||
                "Dynamic languages such as JavaScript"
 | 
			
		||||
              )
 | 
			
		||||
          );
 | 
			
		||||
          const text = await page.evaluate(() =>
 | 
			
		||||
            navigator.clipboard.readText()
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          expect(
 | 
			
		||||
            text.includes("This section provides an overview of our system")
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(
 | 
			
		||||
            text.includes(
 | 
			
		||||
              "are represented by function calls. This makes the LIR used by"
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(
 | 
			
		||||
            text.includes("When compiling loops, we consult the oracle before")
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(text.includes("Nested Trace Tree Formation"))
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(
 | 
			
		||||
            text.includes(
 | 
			
		||||
              "An important detail is that the call to the inner trace"
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(text.includes("When trace recording is completed, nanojit"))
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(
 | 
			
		||||
            text.includes(
 | 
			
		||||
              "SpiderMonkey, like many VMs, needs to preempt the user program"
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(
 | 
			
		||||
            text.includes(
 | 
			
		||||
              "Using similar computations, we find that trace recording takes"
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(
 | 
			
		||||
            text.includes(
 | 
			
		||||
              "specialization algorithm. We also described our trace compiler"
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
          expect(
 | 
			
		||||
            text.includes(
 | 
			
		||||
              "dynamic optimization system. In Proceedings of the ACM SIGPLAN"
 | 
			
		||||
            )
 | 
			
		||||
          )
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual(true);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
  describe("Copy/paste and ligatures", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "copy_paste_ligatures.pdf",
 | 
			
		||||
        "#hiddenCopyElement",
 | 
			
		||||
        100
 | 
			
		||||
      );
 | 
			
		||||
      await mockClipboard(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the ligatures have been removed when the text has been copied", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            ".page[data-page-number='1'] .textLayer .endOfContent"
 | 
			
		||||
          );
 | 
			
		||||
          await selectAll(page);
 | 
			
		||||
 | 
			
		||||
          await copy(page);
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector('#viewerContainer').style.cursor !== "wait"`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            async () => !!(await navigator.clipboard.readText())
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const text = await page.evaluate(() =>
 | 
			
		||||
            navigator.clipboard.readText()
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          expect(text)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toEqual("abcdeffffiflffifflſtstghijklmno");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										97
									
								
								test/integration/document_properties_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								test/integration/document_properties_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
/* Copyright 2024 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 { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
const FIELDS = [
 | 
			
		||||
  "fileName",
 | 
			
		||||
  "fileSize",
 | 
			
		||||
  "title",
 | 
			
		||||
  "author",
 | 
			
		||||
  "subject",
 | 
			
		||||
  "keywords",
 | 
			
		||||
  "creationDate",
 | 
			
		||||
  "modificationDate",
 | 
			
		||||
  "creator",
 | 
			
		||||
  "producer",
 | 
			
		||||
  "version",
 | 
			
		||||
  "pageCount",
 | 
			
		||||
  "pageSize",
 | 
			
		||||
  "linearized",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
describe("PDFDocumentProperties", () => {
 | 
			
		||||
  async function getFieldProperties(page) {
 | 
			
		||||
    const promises = [];
 | 
			
		||||
 | 
			
		||||
    for (const name of FIELDS) {
 | 
			
		||||
      promises.push(
 | 
			
		||||
        page.evaluate(
 | 
			
		||||
          n => [n, document.getElementById(`${n}Field`).textContent],
 | 
			
		||||
          name
 | 
			
		||||
        )
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    return Object.fromEntries(await Promise.all(promises));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  describe("Document with both /Info and /Metadata", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("basicapi.pdf", ".textLayer .endOfContent");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the document properties dialog has the correct information", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.click("#secondaryToolbarToggleButton");
 | 
			
		||||
          await page.waitForSelector("#secondaryToolbar", { hidden: false });
 | 
			
		||||
 | 
			
		||||
          await page.click("#documentProperties");
 | 
			
		||||
          await page.waitForSelector("#documentPropertiesDialog", {
 | 
			
		||||
            hidden: false,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.getElementById("fileSizeField").textContent !== "-"`
 | 
			
		||||
          );
 | 
			
		||||
          const props = await getFieldProperties(page);
 | 
			
		||||
 | 
			
		||||
          expect(props).toEqual({
 | 
			
		||||
            fileName: "basicapi.pdf",
 | 
			
		||||
            fileSize: `${FSI}103${PDI} KB (${FSI}105,779${PDI} bytes)`,
 | 
			
		||||
            title: "Basic API Test",
 | 
			
		||||
            author: "Brendan Dahl",
 | 
			
		||||
            subject: "-",
 | 
			
		||||
            keywords: "TCPDF",
 | 
			
		||||
            creationDate: "4/10/12, 7:30:26 AM",
 | 
			
		||||
            modificationDate: "4/10/12, 7:30:26 AM",
 | 
			
		||||
            creator: "TCPDF",
 | 
			
		||||
            producer: "TCPDF 5.9.133 (http://www.tcpdf.org)",
 | 
			
		||||
            version: "1.7",
 | 
			
		||||
            pageCount: "3",
 | 
			
		||||
            pageSize: `${FSI}8.27${PDI} × ${FSI}11.69${PDI} ${FSI}in${PDI} (${FSI}A4${PDI}, ${FSI}portrait${PDI})`,
 | 
			
		||||
            linearized: "No",
 | 
			
		||||
          });
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										180
									
								
								test/integration/find_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										180
									
								
								test/integration/find_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,180 @@
 | 
			
		||||
/* Copyright 2021 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 { closePages, FSI, loadAndWait, PDI } from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
function fuzzyMatch(a, b, browserName, pixelFuzz = 3) {
 | 
			
		||||
  expect(a)
 | 
			
		||||
    .withContext(`In ${browserName}`)
 | 
			
		||||
    .toBeLessThan(b + pixelFuzz);
 | 
			
		||||
  expect(a)
 | 
			
		||||
    .withContext(`In ${browserName}`)
 | 
			
		||||
    .toBeGreaterThan(b - pixelFuzz);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
describe("find bar", () => {
 | 
			
		||||
  describe("highlight all", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("find_all.pdf", ".textLayer", 100);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must highlight text in the right position", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          // Highlight all occurrences of the letter A (case insensitive).
 | 
			
		||||
          await page.click("#viewFindButton");
 | 
			
		||||
          await page.waitForSelector("#findInput", { visible: true });
 | 
			
		||||
          await page.type("#findInput", "a");
 | 
			
		||||
          await page.click("#findHighlightAll + label");
 | 
			
		||||
          await page.waitForSelector(".textLayer .highlight");
 | 
			
		||||
 | 
			
		||||
          // The PDF file contains the text 'AB BA' in a monospace font on a
 | 
			
		||||
          // single line. Check if the two occurrences of A are highlighted.
 | 
			
		||||
          const highlights = await page.$$(".textLayer .highlight");
 | 
			
		||||
          expect(highlights.length).withContext(`In ${browserName}`).toEqual(2);
 | 
			
		||||
 | 
			
		||||
          // Normalize the highlight's height. The font data in the PDF sets the
 | 
			
		||||
          // size of the glyphs (and therefore the size of the highlights), but
 | 
			
		||||
          // the viewer applies extra padding to them. For the comparison we
 | 
			
		||||
          // therefore use the unpadded, glyph-sized parent element's height.
 | 
			
		||||
          const parentSpan = (await highlights[0].$$("xpath/.."))[0];
 | 
			
		||||
          const parentBox = await parentSpan.boundingBox();
 | 
			
		||||
          const firstA = await highlights[0].boundingBox();
 | 
			
		||||
          const secondA = await highlights[1].boundingBox();
 | 
			
		||||
          firstA.height = parentBox.height;
 | 
			
		||||
          secondA.height = parentBox.height;
 | 
			
		||||
 | 
			
		||||
          // Check if the vertical position of the highlights is correct. Both
 | 
			
		||||
          // should be on a single line.
 | 
			
		||||
          expect(firstA.y).withContext(`In ${browserName}`).toEqual(secondA.y);
 | 
			
		||||
 | 
			
		||||
          // Check if the height of the two highlights is correct. Both should
 | 
			
		||||
          // match the font size.
 | 
			
		||||
          const fontSize = 26.66; // From the PDF.
 | 
			
		||||
          fuzzyMatch(firstA.height, fontSize, browserName);
 | 
			
		||||
          fuzzyMatch(secondA.height, fontSize, browserName);
 | 
			
		||||
 | 
			
		||||
          // Check if the horizontal position of the highlights is correct. The
 | 
			
		||||
          // second occurrence should be four glyph widths (three letters and
 | 
			
		||||
          // one space) away from the first occurrence.
 | 
			
		||||
          const pageDiv = await page.$(".page canvas");
 | 
			
		||||
          const pageBox = await pageDiv.boundingBox();
 | 
			
		||||
          const expectedFirstAX = 30; // From the PDF.
 | 
			
		||||
          const glyphWidth = 15.98; // From the PDF.
 | 
			
		||||
          fuzzyMatch(firstA.x, pageBox.x + expectedFirstAX, browserName);
 | 
			
		||||
          fuzzyMatch(secondA.x, firstA.x + glyphWidth * 4, browserName);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("highlight all (XFA)", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("xfa_imm5257e.pdf", ".xfaLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must search xfa correctly", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.click("#viewFindButton");
 | 
			
		||||
          await page.waitForSelector("#findInput", { visible: true });
 | 
			
		||||
          await page.type("#findInput", "preferences");
 | 
			
		||||
          await page.waitForSelector("#findInput[data-status='']");
 | 
			
		||||
          await page.waitForSelector(".xfaLayer .highlight");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            () => !!document.querySelector("#findResultsCount")?.textContent
 | 
			
		||||
          );
 | 
			
		||||
          const resultElement = await page.waitForSelector("#findResultsCount");
 | 
			
		||||
          const resultText = await resultElement.evaluate(el => el.textContent);
 | 
			
		||||
          expect(resultText).toEqual(`${FSI}1${PDI} of ${FSI}1${PDI} match`);
 | 
			
		||||
          const selectedElement = await page.waitForSelector(
 | 
			
		||||
            ".highlight.selected"
 | 
			
		||||
          );
 | 
			
		||||
          const selectedText = await selectedElement.evaluate(
 | 
			
		||||
            el => el.textContent
 | 
			
		||||
          );
 | 
			
		||||
          expect(selectedText).toEqual("Preferences");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("issue19207.pdf", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("issue19207.pdf", ".textLayer", 200);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must scroll to the search result text", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          // Search for "40"
 | 
			
		||||
          await page.click("#viewFindButton");
 | 
			
		||||
          await page.waitForSelector("#findInput", { visible: true });
 | 
			
		||||
          await page.type("#findInput", "40");
 | 
			
		||||
 | 
			
		||||
          const highlight = await page.waitForSelector(".textLayer .highlight");
 | 
			
		||||
 | 
			
		||||
          expect(await highlight.isIntersectingViewport()).toBeTrue();
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("scrolls to the search result text for smaller viewports", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("tracemonkey.pdf", ".textLayer", 100);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must scroll to the search result text", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          // Set a smaller viewport to simulate a mobile device
 | 
			
		||||
          await page.setViewport({ width: 350, height: 600 });
 | 
			
		||||
          await page.click("#viewFindButton");
 | 
			
		||||
          await page.waitForSelector("#findInput", { visible: true });
 | 
			
		||||
          await page.type("#findInput", "productivity");
 | 
			
		||||
 | 
			
		||||
          const highlight = await page.waitForSelector(".textLayer .highlight");
 | 
			
		||||
 | 
			
		||||
          expect(await highlight.isIntersectingViewport()).toBeTrue();
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										3602
									
								
								test/integration/freetext_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3602
									
								
								test/integration/freetext_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										3027
									
								
								test/integration/highlight_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3027
									
								
								test/integration/highlight_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										1345
									
								
								test/integration/ink_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1345
									
								
								test/integration/ink_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										83
									
								
								test/integration/jasmine-boot.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								test/integration/jasmine-boot.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,83 @@
 | 
			
		||||
/* Copyright 2020 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.
 | 
			
		||||
 */
 | 
			
		||||
 | 
			
		||||
/* eslint-disable no-console */
 | 
			
		||||
 | 
			
		||||
import Jasmine from "jasmine";
 | 
			
		||||
 | 
			
		||||
async function runTests(results) {
 | 
			
		||||
  const jasmine = new Jasmine();
 | 
			
		||||
  jasmine.exitOnCompletion = false;
 | 
			
		||||
  jasmine.jasmine.DEFAULT_TIMEOUT_INTERVAL = 30000;
 | 
			
		||||
 | 
			
		||||
  jasmine.loadConfig({
 | 
			
		||||
    random: true,
 | 
			
		||||
    spec_dir: "integration",
 | 
			
		||||
    spec_files: [
 | 
			
		||||
      "accessibility_spec.mjs",
 | 
			
		||||
      "annotation_spec.mjs",
 | 
			
		||||
      "autolinker_spec.mjs",
 | 
			
		||||
      "caret_browsing_spec.mjs",
 | 
			
		||||
      "comment_spec.mjs",
 | 
			
		||||
      "copy_paste_spec.mjs",
 | 
			
		||||
      "document_properties_spec.mjs",
 | 
			
		||||
      "find_spec.mjs",
 | 
			
		||||
      "freetext_editor_spec.mjs",
 | 
			
		||||
      "highlight_editor_spec.mjs",
 | 
			
		||||
      "ink_editor_spec.mjs",
 | 
			
		||||
      "scripting_spec.mjs",
 | 
			
		||||
      "signature_editor_spec.mjs",
 | 
			
		||||
      "stamp_editor_spec.mjs",
 | 
			
		||||
      "text_field_spec.mjs",
 | 
			
		||||
      "text_layer_spec.mjs",
 | 
			
		||||
      "thumbnail_view_spec.mjs",
 | 
			
		||||
      "viewer_spec.mjs",
 | 
			
		||||
    ],
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  jasmine.addReporter({
 | 
			
		||||
    jasmineDone(suiteInfo) {},
 | 
			
		||||
    jasmineStarted(suiteInfo) {},
 | 
			
		||||
    specDone(result) {
 | 
			
		||||
      // Report on the result of individual tests.
 | 
			
		||||
      ++results.runs;
 | 
			
		||||
      if (result.failedExpectations.length > 0) {
 | 
			
		||||
        ++results.failures;
 | 
			
		||||
        console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
 | 
			
		||||
      } else {
 | 
			
		||||
        console.log(`TEST-PASSED | ${result.description}`);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    specStarted(result) {},
 | 
			
		||||
    suiteDone(result) {
 | 
			
		||||
      // Report on the result of `afterAll` invocations.
 | 
			
		||||
      if (result.failedExpectations.length > 0) {
 | 
			
		||||
        ++results.failures;
 | 
			
		||||
        console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    suiteStarted(result) {
 | 
			
		||||
      // Report on the result of `beforeAll` invocations.
 | 
			
		||||
      if (result.failedExpectations.length > 0) {
 | 
			
		||||
        ++results.failures;
 | 
			
		||||
        console.log(`TEST-UNEXPECTED-FAIL | ${result.description}`);
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return jasmine.execute();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export { runTests };
 | 
			
		||||
							
								
								
									
										2649
									
								
								test/integration/scripting_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2649
									
								
								test/integration/scripting_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										774
									
								
								test/integration/signature_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										774
									
								
								test/integration/signature_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,774 @@
 | 
			
		||||
/* Copyright 2025 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 {
 | 
			
		||||
  awaitPromise,
 | 
			
		||||
  closePages,
 | 
			
		||||
  copy,
 | 
			
		||||
  FSI,
 | 
			
		||||
  getEditorSelector,
 | 
			
		||||
  getRect,
 | 
			
		||||
  loadAndWait,
 | 
			
		||||
  paste,
 | 
			
		||||
  PDI,
 | 
			
		||||
  switchToEditor,
 | 
			
		||||
  waitForPointerUp,
 | 
			
		||||
  waitForTimeout,
 | 
			
		||||
} from "./test_utils.mjs";
 | 
			
		||||
import fs from "fs";
 | 
			
		||||
import path from "path";
 | 
			
		||||
import { PNG } from "pngjs";
 | 
			
		||||
 | 
			
		||||
const __dirname = import.meta.dirname;
 | 
			
		||||
 | 
			
		||||
const switchToSignature = switchToEditor.bind(null, "Signature");
 | 
			
		||||
 | 
			
		||||
describe("Signature Editor", () => {
 | 
			
		||||
  const descriptionInputSelector = "#addSignatureDescription > input";
 | 
			
		||||
  const addButtonSelector = "#addSignatureAddButton";
 | 
			
		||||
 | 
			
		||||
  describe("Basic operations", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the editor has been removed when the dialog is cancelled", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([_, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          // An invisible editor is created but invisible.
 | 
			
		||||
          const editorSelector = getEditorSelector(0);
 | 
			
		||||
          await page.waitForSelector(editorSelector, { visible: false });
 | 
			
		||||
 | 
			
		||||
          await page.click("#addSignatureCancelButton");
 | 
			
		||||
 | 
			
		||||
          // The editor should have been removed.
 | 
			
		||||
          await page.waitForSelector(`:not(${editorSelector})`);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the basic and common elements are working as expected", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureTypeButton[aria-selected=true]"
 | 
			
		||||
          );
 | 
			
		||||
          await page.click("#addSignatureTypeInput");
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureSaveContainer > input:disabled"
 | 
			
		||||
          );
 | 
			
		||||
          let description = await page.$eval(
 | 
			
		||||
            descriptionInputSelector,
 | 
			
		||||
            el => el.value
 | 
			
		||||
          );
 | 
			
		||||
          expect(description).withContext(browserName).toEqual("");
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:disabled`);
 | 
			
		||||
          await page.waitForSelector("#addSignatureDescInput:disabled");
 | 
			
		||||
 | 
			
		||||
          await page.type("#addSignatureTypeInput", "PDF.js");
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
 | 
			
		||||
          await page.waitForSelector("#addSignatureDescInput:not(:disabled)");
 | 
			
		||||
 | 
			
		||||
          // The save button should be enabled now.
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureSaveContainer > input:not(:disabled)"
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector("#addSignatureSaveCheckbox:checked");
 | 
			
		||||
 | 
			
		||||
          // The description has been filled in automatically.
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${descriptionInputSelector}").value !== ""`
 | 
			
		||||
          );
 | 
			
		||||
          description = await page.$eval(
 | 
			
		||||
            descriptionInputSelector,
 | 
			
		||||
            el => el.value
 | 
			
		||||
          );
 | 
			
		||||
          expect(description).withContext(browserName).toEqual("PDF.js");
 | 
			
		||||
 | 
			
		||||
          // Clear the description.
 | 
			
		||||
          await page.click("#addSignatureDescription > button");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${descriptionInputSelector}").value === ""`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Clear the text for the signature.
 | 
			
		||||
          await page.click("#clearSignatureButton");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("#addSignatureTypeInput").value === ""`
 | 
			
		||||
          );
 | 
			
		||||
          // The save button should be disabled now.
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureSaveContainer > input:disabled"
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:disabled`);
 | 
			
		||||
 | 
			
		||||
          await page.type("#addSignatureTypeInput", "PDF.js");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${descriptionInputSelector}").value !== ""`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Clearing the signature type should clear the description.
 | 
			
		||||
          await page.click("#clearSignatureButton");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("#addSignatureTypeInput").value === ""`
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${descriptionInputSelector}").value === ""`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Add a signature and change the description.
 | 
			
		||||
          await page.type("#addSignatureTypeInput", "PDF.js");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${descriptionInputSelector}").value !== ""`
 | 
			
		||||
          );
 | 
			
		||||
          await page.click("#addSignatureDescription > button");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${descriptionInputSelector}").value === ""`
 | 
			
		||||
          );
 | 
			
		||||
          await page.type(descriptionInputSelector, "Hello World");
 | 
			
		||||
          await page.type("#addSignatureTypeInput", "Hello");
 | 
			
		||||
 | 
			
		||||
          // The description mustn't be changed.
 | 
			
		||||
          // eslint-disable-next-line no-restricted-syntax
 | 
			
		||||
          await waitForTimeout(100);
 | 
			
		||||
          description = await page.$eval(
 | 
			
		||||
            descriptionInputSelector,
 | 
			
		||||
            el => el.value
 | 
			
		||||
          );
 | 
			
		||||
          expect(description).withContext(browserName).toEqual("Hello World");
 | 
			
		||||
 | 
			
		||||
          await page.click("#addSignatureAddButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: false,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          const editorSelector = getEditorSelector(0);
 | 
			
		||||
          await page.waitForSelector(editorSelector, { visible: true });
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `.canvasWrapper > svg use[href="#path_p1_0"]`,
 | 
			
		||||
            { visible: true }
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.getElementById("viewer-alert").textContent === "Signature added"`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Check the tooltip.
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `.altText.editDescription[title="Hello World"]`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Check the aria description.
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `${editorSelector}[aria-description="Signature editor: ${FSI}Hello World${PDI}"]`
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          // Edit the description.
 | 
			
		||||
          await page.click(`.altText.editDescription`);
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#editSignatureDescriptionDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.waitForSelector("#editSignatureUpdateButton:disabled");
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `#editSignatureDescriptionDialog svg[aria-label="Hello World"]`
 | 
			
		||||
          );
 | 
			
		||||
          const editDescriptionInput = "#editSignatureDescription > input";
 | 
			
		||||
          description = await page.$eval(editDescriptionInput, el => el.value);
 | 
			
		||||
          expect(description).withContext(browserName).toEqual("Hello World");
 | 
			
		||||
          await page.click("#editSignatureDescription > button");
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${editDescriptionInput}").value === ""`
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#editSignatureUpdateButton:not(:disabled)"
 | 
			
		||||
          );
 | 
			
		||||
          await page.type(editDescriptionInput, "Hello PDF.js World");
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#editSignatureUpdateButton:not(:disabled)"
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          await page.click("#editSignatureUpdateButton");
 | 
			
		||||
 | 
			
		||||
          // Check the tooltip.
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `.altText.editDescription[title="Hello PDF.js World"]`
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check drawing with the mouse", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await page.click("#addSignatureDrawButton");
 | 
			
		||||
          const drawSelector = "#addSignatureDraw";
 | 
			
		||||
          await page.waitForSelector(drawSelector, { visible: true });
 | 
			
		||||
 | 
			
		||||
          let description = await page.$eval(
 | 
			
		||||
            descriptionInputSelector,
 | 
			
		||||
            el => el.value
 | 
			
		||||
          );
 | 
			
		||||
          expect(description).withContext(browserName).toEqual("");
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:disabled`);
 | 
			
		||||
 | 
			
		||||
          const { x, y, width, height } = await getRect(page, drawSelector);
 | 
			
		||||
          const clickHandle = await waitForPointerUp(page);
 | 
			
		||||
          await page.mouse.move(x + 0.1 * width, y + 0.1 * height);
 | 
			
		||||
          await page.mouse.down();
 | 
			
		||||
          await page.mouse.move(x + 0.3 * width, y + 0.3 * height);
 | 
			
		||||
          await page.mouse.up();
 | 
			
		||||
          await awaitPromise(clickHandle);
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
 | 
			
		||||
 | 
			
		||||
          // The save button should be enabled now.
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureSaveContainer > input:not(:disabled)"
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector("#addSignatureSaveCheckbox:checked");
 | 
			
		||||
 | 
			
		||||
          // The description has been filled in automatically.
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${descriptionInputSelector}").value !== ""`
 | 
			
		||||
          );
 | 
			
		||||
          description = await page.$eval(
 | 
			
		||||
            descriptionInputSelector,
 | 
			
		||||
            el => el.value
 | 
			
		||||
          );
 | 
			
		||||
          expect(description).withContext(browserName).toEqual("Signature");
 | 
			
		||||
 | 
			
		||||
          await page.click("#addSignatureAddButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: false,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            ".canvasWrapper > svg use[href='#path_p1_0']"
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check adding an image", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await page.click("#addSignatureImageButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureImagePlaceholder", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          let description = await page.$eval(
 | 
			
		||||
            descriptionInputSelector,
 | 
			
		||||
            el => el.value
 | 
			
		||||
          );
 | 
			
		||||
          expect(description).withContext(browserName).toEqual("");
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:disabled`);
 | 
			
		||||
 | 
			
		||||
          const input = await page.$("#addSignatureFilePicker");
 | 
			
		||||
          await input.uploadFile(
 | 
			
		||||
            `${path.join(__dirname, "../images/firefox_logo.png")}`
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector(`#addSignatureImage > path:not([d=""])`);
 | 
			
		||||
 | 
			
		||||
          // The save button should be enabled now.
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureSaveContainer > input:not(:disabled)"
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector("#addSignatureSaveCheckbox:checked");
 | 
			
		||||
 | 
			
		||||
          // The description has been filled in automatically.
 | 
			
		||||
          await page.waitForFunction(
 | 
			
		||||
            `document.querySelector("${descriptionInputSelector}").value !== ""`
 | 
			
		||||
          );
 | 
			
		||||
          description = await page.$eval(
 | 
			
		||||
            descriptionInputSelector,
 | 
			
		||||
            el => el.value
 | 
			
		||||
          );
 | 
			
		||||
          expect(description)
 | 
			
		||||
            .withContext(browserName)
 | 
			
		||||
            .toEqual("firefox_logo.png");
 | 
			
		||||
 | 
			
		||||
          await page.click("#addSignatureAddButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: false,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            ".canvasWrapper > svg use[href='#path_p1_0']"
 | 
			
		||||
          );
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check copy and paste", async () => {
 | 
			
		||||
      // Run sequentially to avoid clipboard issues.
 | 
			
		||||
      for (const [browserName, page] of pages) {
 | 
			
		||||
        await switchToSignature(page);
 | 
			
		||||
        await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
        await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
          visible: true,
 | 
			
		||||
        });
 | 
			
		||||
        await page.type("#addSignatureTypeInput", "Hello");
 | 
			
		||||
        await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
 | 
			
		||||
        await page.click("#addSignatureAddButton");
 | 
			
		||||
 | 
			
		||||
        const editorSelector = getEditorSelector(0);
 | 
			
		||||
        await page.waitForSelector(editorSelector, { visible: true });
 | 
			
		||||
        const originalRect = await getRect(page, editorSelector);
 | 
			
		||||
        const originalDescription = await page.$eval(
 | 
			
		||||
          `${editorSelector} .altText.editDescription`,
 | 
			
		||||
          el => el.title
 | 
			
		||||
        );
 | 
			
		||||
        const originalL10nParameter = await page.$eval(editorSelector, el =>
 | 
			
		||||
          el.getAttribute("data-l10n-args")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        await copy(page);
 | 
			
		||||
        await paste(page);
 | 
			
		||||
 | 
			
		||||
        const pastedEditorSelector = getEditorSelector(1);
 | 
			
		||||
        await page.waitForSelector(pastedEditorSelector, { visible: true });
 | 
			
		||||
        const pastedRect = await getRect(page, pastedEditorSelector);
 | 
			
		||||
        const pastedDescription = await page.$eval(
 | 
			
		||||
          `${pastedEditorSelector} .altText.editDescription`,
 | 
			
		||||
          el => el.title
 | 
			
		||||
        );
 | 
			
		||||
        const pastedL10nParameter = await page.$eval(pastedEditorSelector, el =>
 | 
			
		||||
          el.getAttribute("data-l10n-args")
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        expect(pastedRect)
 | 
			
		||||
          .withContext(`In ${browserName}`)
 | 
			
		||||
          .not.toEqual(originalRect);
 | 
			
		||||
        expect(pastedDescription)
 | 
			
		||||
          .withContext(`In ${browserName}`)
 | 
			
		||||
          .toEqual(originalDescription);
 | 
			
		||||
        expect(pastedL10nParameter)
 | 
			
		||||
          .withContext(`In ${browserName}`)
 | 
			
		||||
          .toEqual(originalL10nParameter);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Bug 1948741", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the editor isn't too large", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([_, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.type(
 | 
			
		||||
            "#addSignatureTypeInput",
 | 
			
		||||
            "[18:50:03] asset pdf.scripting.mjs 105 KiB [emitted] [javascript module] (name: main)"
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
 | 
			
		||||
          await page.click("#addSignatureAddButton");
 | 
			
		||||
 | 
			
		||||
          const editorSelector = getEditorSelector(0);
 | 
			
		||||
          await page.waitForSelector(editorSelector, { visible: true });
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `.canvasWrapper > svg use[href="#path_p1_0"]`,
 | 
			
		||||
            { visible: true }
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const { width } = await getRect(page, editorSelector);
 | 
			
		||||
          const { width: pageWidth } = await getRect(page, ".page");
 | 
			
		||||
          expect(width).toBeLessThanOrEqual(pageWidth);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Bug 1949201", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the error panel is correctly removed", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([_, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.click("#addSignatureImageButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureImagePlaceholder", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          const input = await page.$("#addSignatureFilePicker");
 | 
			
		||||
          await input.uploadFile(
 | 
			
		||||
            `${path.join(__dirname, "./signature_editor_spec.mjs")}`
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector("#addSignatureError", { visible: true });
 | 
			
		||||
          await page.click("#addSignatureErrorCloseButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureError", { visible: false });
 | 
			
		||||
 | 
			
		||||
          await input.uploadFile(
 | 
			
		||||
            `${path.join(__dirname, "./stamp_editor_spec.mjs")}`
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector("#addSignatureError", { visible: true });
 | 
			
		||||
 | 
			
		||||
          await page.click("#addSignatureTypeButton");
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureTypeButton[aria-selected=true]"
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector("#addSignatureError", { visible: false });
 | 
			
		||||
          await page.click("#addSignatureCancelButton");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("viewerCssTheme (light)", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "empty.pdf",
 | 
			
		||||
        ".annotationEditorLayer",
 | 
			
		||||
        null,
 | 
			
		||||
        null,
 | 
			
		||||
        { viewerCssTheme: "1" }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the signature has the correct color with the light theme", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([_, page]) => {
 | 
			
		||||
          const colorTheme = await page.evaluate(() => {
 | 
			
		||||
            const html = document.querySelector("html");
 | 
			
		||||
            const style = getComputedStyle(html);
 | 
			
		||||
            return style.getPropertyValue("color-scheme");
 | 
			
		||||
          });
 | 
			
		||||
          expect(colorTheme).toEqual("light");
 | 
			
		||||
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.type("#addSignatureTypeInput", "Should be black.");
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
 | 
			
		||||
          await page.click("#addSignatureAddButton");
 | 
			
		||||
 | 
			
		||||
          const editorSelector = getEditorSelector(0);
 | 
			
		||||
          await page.waitForSelector(editorSelector, { visible: true });
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `.canvasWrapper > svg use[href="#path_p1_0"]`,
 | 
			
		||||
            { visible: true }
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const color = await page.evaluate(() => {
 | 
			
		||||
            const use = document.querySelector(
 | 
			
		||||
              `.canvasWrapper > svg use[href="#path_p1_0"]`
 | 
			
		||||
            );
 | 
			
		||||
            return use.parentNode.getAttribute("fill");
 | 
			
		||||
          });
 | 
			
		||||
          expect(color).toEqual("#000000");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("viewerCssTheme (dark)", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait(
 | 
			
		||||
        "empty.pdf",
 | 
			
		||||
        ".annotationEditorLayer",
 | 
			
		||||
        null,
 | 
			
		||||
        null,
 | 
			
		||||
        { viewerCssTheme: "2" }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the signature has the correct color with the dark theme", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([_, page]) => {
 | 
			
		||||
          const colorTheme = await page.evaluate(() => {
 | 
			
		||||
            const html = document.querySelector("html");
 | 
			
		||||
            const style = getComputedStyle(html);
 | 
			
		||||
            return style.getPropertyValue("color-scheme");
 | 
			
		||||
          });
 | 
			
		||||
          expect(colorTheme).toEqual("dark");
 | 
			
		||||
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.type("#addSignatureTypeInput", "Should be black.");
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:not(:disabled)`);
 | 
			
		||||
          await page.click("#addSignatureAddButton");
 | 
			
		||||
 | 
			
		||||
          const editorSelector = getEditorSelector(0);
 | 
			
		||||
          await page.waitForSelector(editorSelector, { visible: true });
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            `.canvasWrapper > svg use[href="#path_p1_0"]`,
 | 
			
		||||
            { visible: true }
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const color = await page.evaluate(() => {
 | 
			
		||||
            const use = document.querySelector(
 | 
			
		||||
              `.canvasWrapper > svg use[href="#path_p1_0"]`
 | 
			
		||||
            );
 | 
			
		||||
            return use.parentNode.getAttribute("fill");
 | 
			
		||||
          });
 | 
			
		||||
          expect(color).toEqual("#000000");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Check the aspect ratio (bug 1962819)", () => {
 | 
			
		||||
    let pages, contentWidth, contentHeight;
 | 
			
		||||
 | 
			
		||||
    function getContentAspectRatio(png) {
 | 
			
		||||
      const { width, height } = png;
 | 
			
		||||
      const buffer = new Uint32Array(png.data.buffer);
 | 
			
		||||
      let x0 = width;
 | 
			
		||||
      let y0 = height;
 | 
			
		||||
      let x1 = 0;
 | 
			
		||||
      let y1 = 0;
 | 
			
		||||
      for (let i = 0; i < height; i++) {
 | 
			
		||||
        for (let j = 0; j < width; j++) {
 | 
			
		||||
          if (buffer[width * i + j] !== 0) {
 | 
			
		||||
            x0 = Math.min(x0, j);
 | 
			
		||||
            y0 = Math.min(y0, i);
 | 
			
		||||
            x1 = Math.max(x1, j);
 | 
			
		||||
            y1 = Math.max(y1, i);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      contentWidth = x1 - x0;
 | 
			
		||||
      contentHeight = y1 - y0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeAll(() => {
 | 
			
		||||
      const data = fs.readFileSync(
 | 
			
		||||
        path.join(__dirname, "../images/samplesignature.png")
 | 
			
		||||
      );
 | 
			
		||||
      const png = PNG.sync.read(data);
 | 
			
		||||
      getContentAspectRatio(png);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the signature has the correct aspect ratio", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
 | 
			
		||||
          await page.click("#addSignatureImageButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureImagePlaceholder", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.waitForSelector(`${addButtonSelector}:disabled`);
 | 
			
		||||
 | 
			
		||||
          const input = await page.$("#addSignatureFilePicker");
 | 
			
		||||
          await input.uploadFile(
 | 
			
		||||
            `${path.join(__dirname, "../images/samplesignature.png")}`
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector(`#addSignatureImage > path:not([d=""])`);
 | 
			
		||||
 | 
			
		||||
          // The save button should be enabled now.
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureSaveContainer > input:not(:disabled)"
 | 
			
		||||
          );
 | 
			
		||||
          await page.click("#addSignatureAddButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: false,
 | 
			
		||||
          });
 | 
			
		||||
          const { width, height } = await getRect(
 | 
			
		||||
            page,
 | 
			
		||||
            ".canvasWrapper > svg use[href='#path_p1_0']"
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          expect(Math.abs(contentWidth / width - contentHeight / height))
 | 
			
		||||
            .withContext(
 | 
			
		||||
              `In ${browserName} (${contentWidth}x${contentHeight} vs ${width}x${height})`
 | 
			
		||||
            )
 | 
			
		||||
            .toBeLessThan(0.25);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Bug 1974257", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the signature save checkbox is disabled if storage is full", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([_, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
 | 
			
		||||
          for (let i = 0; i < 6; i++) {
 | 
			
		||||
            await page.click("#editorSignatureAddSignature");
 | 
			
		||||
            await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
              visible: true,
 | 
			
		||||
            });
 | 
			
		||||
            await page.click("#addSignatureTypeInput");
 | 
			
		||||
            await page.type("#addSignatureTypeInput", `PDF.js ${i}`);
 | 
			
		||||
            if (i === 5) {
 | 
			
		||||
              await page.waitForSelector(
 | 
			
		||||
                "#addSignatureSaveCheckbox:not(checked)"
 | 
			
		||||
              );
 | 
			
		||||
              await page.waitForSelector("#addSignatureSaveCheckbox:disabled");
 | 
			
		||||
            } else {
 | 
			
		||||
              await page.waitForSelector("#addSignatureSaveCheckbox:checked");
 | 
			
		||||
              await page.waitForSelector(
 | 
			
		||||
                "#addSignatureSaveCheckbox:not(:disabled)"
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
            await page.click("#addSignatureAddButton");
 | 
			
		||||
            await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
              visible: false,
 | 
			
		||||
            });
 | 
			
		||||
          }
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("Bug 1975719", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("empty.pdf", ".annotationEditorLayer");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that an error is displayed with a monochrome image", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([_, page]) => {
 | 
			
		||||
          await switchToSignature(page);
 | 
			
		||||
 | 
			
		||||
          await page.click("#editorSignatureAddSignature");
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector("#addSignatureDialog", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          await page.click("#addSignatureImageButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureImagePlaceholder", {
 | 
			
		||||
            visible: true,
 | 
			
		||||
          });
 | 
			
		||||
          const input = await page.$("#addSignatureFilePicker");
 | 
			
		||||
          await input.uploadFile(
 | 
			
		||||
            `${path.join(__dirname, "../images/red.png")}`
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector("#addSignatureError", { visible: true });
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureErrorTitle[data-l10n-id='pdfjs-editor-add-signature-image-no-data-error-title']"
 | 
			
		||||
          );
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            "#addSignatureErrorDescription[data-l10n-id='pdfjs-editor-add-signature-image-no-data-error-description']"
 | 
			
		||||
          );
 | 
			
		||||
          await page.click("#addSignatureErrorCloseButton");
 | 
			
		||||
          await page.waitForSelector("#addSignatureError", { visible: false });
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										1827
									
								
								test/integration/stamp_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1827
									
								
								test/integration/stamp_editor_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										964
									
								
								test/integration/test_utils.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										964
									
								
								test/integration/test_utils.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,964 @@
 | 
			
		||||
/* Copyright 2020 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 os from "os";
 | 
			
		||||
 | 
			
		||||
const isMac = os.platform() === "darwin";
 | 
			
		||||
 | 
			
		||||
function loadAndWait(filename, selector, zoom, setups, options, viewport) {
 | 
			
		||||
  return Promise.all(
 | 
			
		||||
    global.integrationSessions.map(async session => {
 | 
			
		||||
      const page = await session.browser.newPage();
 | 
			
		||||
 | 
			
		||||
      if (viewport) {
 | 
			
		||||
        await page.setViewport(viewport);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // In order to avoid errors because of checks which depend on
 | 
			
		||||
      // a locale.
 | 
			
		||||
      await page.evaluateOnNewDocument(() => {
 | 
			
		||||
        Object.defineProperty(navigator, "language", {
 | 
			
		||||
          get() {
 | 
			
		||||
            return "en-US";
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
        Object.defineProperty(navigator, "languages", {
 | 
			
		||||
          get() {
 | 
			
		||||
            return ["en-US", "en"];
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      let app_options = "";
 | 
			
		||||
      if (options) {
 | 
			
		||||
        const optionsObject =
 | 
			
		||||
          typeof options === "function"
 | 
			
		||||
            ? await options(page, session.name)
 | 
			
		||||
            : options;
 | 
			
		||||
 | 
			
		||||
        // Options must be handled in app.js::_parseHashParams.
 | 
			
		||||
        for (const [key, value] of Object.entries(optionsObject)) {
 | 
			
		||||
          app_options += `&${key}=${encodeURIComponent(value)}`;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const fileParam = filename.startsWith("http")
 | 
			
		||||
        ? filename
 | 
			
		||||
        : `/test/pdfs/${filename}`;
 | 
			
		||||
      const url = `${global.integrationBaseUrl}?file=${fileParam}#zoom=${zoom ?? "page-fit"}${app_options}`;
 | 
			
		||||
 | 
			
		||||
      if (setups) {
 | 
			
		||||
        // page.evaluateOnNewDocument allows us to run code before the
 | 
			
		||||
        // first js script is executed.
 | 
			
		||||
        // The idea here is to set up some setters for PDFViewerApplication
 | 
			
		||||
        // and EventBus, so we can inject some code to do whatever we want
 | 
			
		||||
        // soon enough especially before the first event in the eventBus is
 | 
			
		||||
        // dispatched.
 | 
			
		||||
        const { prePageSetup, appSetup, earlySetup, eventBusSetup } = setups;
 | 
			
		||||
        await prePageSetup?.(page);
 | 
			
		||||
        if (earlySetup || appSetup || eventBusSetup) {
 | 
			
		||||
          await page.evaluateOnNewDocument(
 | 
			
		||||
            (eaSetup, aSetup, evSetup) => {
 | 
			
		||||
              if (eaSetup) {
 | 
			
		||||
                // eslint-disable-next-line no-eval
 | 
			
		||||
                eval(`(${eaSetup})`)();
 | 
			
		||||
              }
 | 
			
		||||
              let app;
 | 
			
		||||
              let eventBus;
 | 
			
		||||
              Object.defineProperty(window, "PDFViewerApplication", {
 | 
			
		||||
                get() {
 | 
			
		||||
                  return app;
 | 
			
		||||
                },
 | 
			
		||||
                set(newValue) {
 | 
			
		||||
                  app = newValue;
 | 
			
		||||
                  if (aSetup) {
 | 
			
		||||
                    // eslint-disable-next-line no-eval
 | 
			
		||||
                    eval(`(${aSetup})`)(app);
 | 
			
		||||
                  }
 | 
			
		||||
                  Object.defineProperty(app, "eventBus", {
 | 
			
		||||
                    get() {
 | 
			
		||||
                      return eventBus;
 | 
			
		||||
                    },
 | 
			
		||||
                    set(newV) {
 | 
			
		||||
                      eventBus = newV;
 | 
			
		||||
                      if (evSetup) {
 | 
			
		||||
                        // eslint-disable-next-line no-eval
 | 
			
		||||
                        eval(`(${evSetup})`)(eventBus);
 | 
			
		||||
                      }
 | 
			
		||||
                    },
 | 
			
		||||
                  });
 | 
			
		||||
                },
 | 
			
		||||
              });
 | 
			
		||||
            },
 | 
			
		||||
            earlySetup?.toString(),
 | 
			
		||||
            appSetup?.toString(),
 | 
			
		||||
            eventBusSetup?.toString()
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      await page.goto(url);
 | 
			
		||||
      await setups?.postPageSetup?.(page);
 | 
			
		||||
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
      if (selector) {
 | 
			
		||||
        await page.waitForSelector(selector, {
 | 
			
		||||
          timeout: 0,
 | 
			
		||||
        });
 | 
			
		||||
      }
 | 
			
		||||
      return [session.name, page];
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function createPromise(page, callback) {
 | 
			
		||||
  return page.evaluateHandle(
 | 
			
		||||
    // eslint-disable-next-line no-eval
 | 
			
		||||
    cb => [new Promise(eval(`(${cb})`))],
 | 
			
		||||
    callback.toString()
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function awaitPromise(promise) {
 | 
			
		||||
  return promise.evaluate(([p]) => p);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function closePages(pages) {
 | 
			
		||||
  return Promise.all(pages.map(([_, page]) => closeSinglePage(page)));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function closeSinglePage(page) {
 | 
			
		||||
  // Avoid to keep something from a previous test.
 | 
			
		||||
  await page.evaluate(async () => {
 | 
			
		||||
    await window.PDFViewerApplication.testingClose();
 | 
			
		||||
    window.localStorage.clear();
 | 
			
		||||
  });
 | 
			
		||||
  await page.close({ runBeforeUnload: false });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function waitForSandboxTrip(page) {
 | 
			
		||||
  const handle = await page.evaluateHandle(() => [
 | 
			
		||||
    new Promise(resolve => {
 | 
			
		||||
      window.addEventListener("sandboxtripend", resolve, { once: true });
 | 
			
		||||
      window.PDFViewerApplication.pdfScriptingManager.sandboxTrip();
 | 
			
		||||
    }),
 | 
			
		||||
  ]);
 | 
			
		||||
  await awaitPromise(handle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForTimeout(milliseconds) {
 | 
			
		||||
  /**
 | 
			
		||||
   * Wait for the given number of milliseconds.
 | 
			
		||||
   *
 | 
			
		||||
   * Note that waiting for an arbitrary time in tests is discouraged because it
 | 
			
		||||
   * can easily cause intermittent failures, which is why this functionality is
 | 
			
		||||
   * no longer provided by Puppeteer 22+ and we have to implement it ourselves
 | 
			
		||||
   * for the remaining callers in the integration tests. We should avoid
 | 
			
		||||
   * creating new usages of this function; instead please refer to the better
 | 
			
		||||
   * alternatives at https://github.com/puppeteer/puppeteer/pull/11780.
 | 
			
		||||
   */
 | 
			
		||||
  return new Promise(resolve => {
 | 
			
		||||
    setTimeout(resolve, milliseconds);
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function clearInput(page, selector, waitForInputEvent = false) {
 | 
			
		||||
  const action = async () => {
 | 
			
		||||
    await page.click(selector);
 | 
			
		||||
    await kbSelectAll(page);
 | 
			
		||||
    await page.keyboard.press("Backspace");
 | 
			
		||||
    await page.waitForFunction(
 | 
			
		||||
      `document.querySelector('${selector}').value === ""`
 | 
			
		||||
    );
 | 
			
		||||
  };
 | 
			
		||||
  return waitForInputEvent
 | 
			
		||||
    ? waitForEvent({
 | 
			
		||||
        page,
 | 
			
		||||
        eventName: "input",
 | 
			
		||||
        action,
 | 
			
		||||
        selector,
 | 
			
		||||
      })
 | 
			
		||||
    : action();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function waitAndClick(page, selector, clickOptions = {}) {
 | 
			
		||||
  await page.waitForSelector(selector, { visible: true });
 | 
			
		||||
  await page.click(selector, clickOptions);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForPointerUp(page) {
 | 
			
		||||
  return createPromise(page, resolve => {
 | 
			
		||||
    window.addEventListener("pointerup", resolve, { once: true });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getSelector(id) {
 | 
			
		||||
  return `[data-element-id="${id}"]`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getRect(page, selector) {
 | 
			
		||||
  // In Chrome something is wrong when serializing a `DomRect`,
 | 
			
		||||
  // so we extract the values and return them ourselves.
 | 
			
		||||
  await page.waitForSelector(selector, { visible: true });
 | 
			
		||||
  return page.$eval(selector, el => {
 | 
			
		||||
    const { x, y, width, height } = el.getBoundingClientRect();
 | 
			
		||||
    return { x, y, width, height };
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getQuerySelector(id) {
 | 
			
		||||
  return `document.querySelector('${getSelector(id)}')`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getComputedStyleSelector(id) {
 | 
			
		||||
  return `getComputedStyle(${getQuerySelector(id)})`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getEditorSelector(n) {
 | 
			
		||||
  return `#pdfjs_internal_editor_${n}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAnnotationSelector(id) {
 | 
			
		||||
  return `[data-annotation-id="${id}"]`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getSpanRectFromText(page, pageNumber, text) {
 | 
			
		||||
  await page.waitForSelector(
 | 
			
		||||
    `.page[data-page-number="${pageNumber}"] > .textLayer .endOfContent`
 | 
			
		||||
  );
 | 
			
		||||
  return page.evaluate(
 | 
			
		||||
    (number, content) => {
 | 
			
		||||
      for (const el of document.querySelectorAll(
 | 
			
		||||
        `.page[data-page-number="${number}"] > .textLayer span:not(:has(> span))`
 | 
			
		||||
      )) {
 | 
			
		||||
        if (el.textContent === content) {
 | 
			
		||||
          const { x, y, width, height } = el.getBoundingClientRect();
 | 
			
		||||
          return { x, y, width, height };
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      return null;
 | 
			
		||||
    },
 | 
			
		||||
    pageNumber,
 | 
			
		||||
    text
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function waitForEvent({
 | 
			
		||||
  page,
 | 
			
		||||
  eventName,
 | 
			
		||||
  action,
 | 
			
		||||
  selector = null,
 | 
			
		||||
  validator = null,
 | 
			
		||||
  timeout = 5000,
 | 
			
		||||
}) {
 | 
			
		||||
  const handle = await page.evaluateHandle(
 | 
			
		||||
    (name, sel, validate, timeOut) => {
 | 
			
		||||
      let callback = null,
 | 
			
		||||
        timeoutId = null;
 | 
			
		||||
      const element = sel ? document.querySelector(sel) : document;
 | 
			
		||||
      return [
 | 
			
		||||
        Promise.race([
 | 
			
		||||
          new Promise(resolve => {
 | 
			
		||||
            // The promise is resolved if the event fired in the context of the
 | 
			
		||||
            // selector and, if a validator is defined, the event data satisfies
 | 
			
		||||
            // the conditions of the validator function.
 | 
			
		||||
            callback = e => {
 | 
			
		||||
              if (timeoutId) {
 | 
			
		||||
                clearTimeout(timeoutId);
 | 
			
		||||
              }
 | 
			
		||||
              // eslint-disable-next-line no-eval
 | 
			
		||||
              resolve(validate ? eval(`(${validate})`)(e) : true);
 | 
			
		||||
            };
 | 
			
		||||
            element.addEventListener(name, callback, { once: true });
 | 
			
		||||
          }),
 | 
			
		||||
          new Promise(resolve => {
 | 
			
		||||
            timeoutId = setTimeout(() => {
 | 
			
		||||
              element.removeEventListener(name, callback);
 | 
			
		||||
              resolve(null);
 | 
			
		||||
            }, timeOut);
 | 
			
		||||
          }),
 | 
			
		||||
        ]),
 | 
			
		||||
      ];
 | 
			
		||||
    },
 | 
			
		||||
    eventName,
 | 
			
		||||
    selector,
 | 
			
		||||
    validator ? validator.toString() : null,
 | 
			
		||||
    timeout
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  await action();
 | 
			
		||||
 | 
			
		||||
  const success = await awaitPromise(handle);
 | 
			
		||||
  if (success === null) {
 | 
			
		||||
    console.warn(
 | 
			
		||||
      `waitForEvent: ${eventName} didn't trigger within the timeout`
 | 
			
		||||
    );
 | 
			
		||||
  } else if (!success) {
 | 
			
		||||
    console.warn(`waitForEvent: ${eventName} triggered, but validation failed`);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function waitForStorageEntries(page, nEntries) {
 | 
			
		||||
  return page.waitForFunction(
 | 
			
		||||
    n => window.PDFViewerApplication.pdfDocument.annotationStorage.size === n,
 | 
			
		||||
    {},
 | 
			
		||||
    nEntries
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function waitForSerialized(page, nEntries) {
 | 
			
		||||
  return page.waitForFunction(
 | 
			
		||||
    n => {
 | 
			
		||||
      try {
 | 
			
		||||
        return (
 | 
			
		||||
          (window.PDFViewerApplication.pdfDocument.annotationStorage
 | 
			
		||||
            .serializable.map?.size ?? 0) === n
 | 
			
		||||
        );
 | 
			
		||||
      } catch {
 | 
			
		||||
        // When serializing a stamp annotation with a SVG, the transfer
 | 
			
		||||
        // can fail because of the SVG, so we just retry.
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    {},
 | 
			
		||||
    nEntries
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function applyFunctionToEditor(page, editorId, func) {
 | 
			
		||||
  return page.evaluate(
 | 
			
		||||
    (id, f) => {
 | 
			
		||||
      const editor =
 | 
			
		||||
        window.PDFViewerApplication.pdfDocument.annotationStorage.getRawValue(
 | 
			
		||||
          id
 | 
			
		||||
        );
 | 
			
		||||
      // eslint-disable-next-line no-eval
 | 
			
		||||
      eval(`(${f})`)(editor);
 | 
			
		||||
    },
 | 
			
		||||
    editorId,
 | 
			
		||||
    func.toString()
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function selectEditor(page, selector, count = 1) {
 | 
			
		||||
  const editorRect = await getRect(page, selector);
 | 
			
		||||
  await page.mouse.click(
 | 
			
		||||
    editorRect.x + editorRect.width / 2,
 | 
			
		||||
    editorRect.y + editorRect.height / 2,
 | 
			
		||||
    { count }
 | 
			
		||||
  );
 | 
			
		||||
  await waitForSelectedEditor(page, selector);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function waitForSelectedEditor(page, selector) {
 | 
			
		||||
  return page.waitForSelector(`${selector}.selectedEditor`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function unselectEditor(page, selector) {
 | 
			
		||||
  await page.keyboard.press("Escape");
 | 
			
		||||
  await waitForUnselectedEditor(page, selector);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function waitForUnselectedEditor(page, selector) {
 | 
			
		||||
  return page.waitForSelector(`${selector}:not(.selectedEditor)`);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function mockClipboard(pages) {
 | 
			
		||||
  return Promise.all(
 | 
			
		||||
    pages.map(async ([_, page]) => {
 | 
			
		||||
      await page.evaluate(() => {
 | 
			
		||||
        let data = null;
 | 
			
		||||
        const clipboard = {
 | 
			
		||||
          writeText: async text => (data = text),
 | 
			
		||||
          readText: async () => data,
 | 
			
		||||
        };
 | 
			
		||||
        Object.defineProperty(navigator, "clipboard", { value: clipboard });
 | 
			
		||||
      });
 | 
			
		||||
    })
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function copy(page) {
 | 
			
		||||
  await waitForEvent({
 | 
			
		||||
    page,
 | 
			
		||||
    eventName: "copy",
 | 
			
		||||
    action: () => kbCopy(page),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function copyToClipboard(page, data) {
 | 
			
		||||
  await page.evaluate(async dat => {
 | 
			
		||||
    const items = Object.create(null);
 | 
			
		||||
    for (const [type, value] of Object.entries(dat)) {
 | 
			
		||||
      if (value.startsWith("data:")) {
 | 
			
		||||
        const resp = await fetch(value);
 | 
			
		||||
        items[type] = await resp.blob();
 | 
			
		||||
      } else {
 | 
			
		||||
        items[type] = new Blob([value], { type });
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    await navigator.clipboard.write([new ClipboardItem(items)]);
 | 
			
		||||
  }, data);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function paste(page) {
 | 
			
		||||
  await waitForEvent({
 | 
			
		||||
    page,
 | 
			
		||||
    eventName: "paste",
 | 
			
		||||
    action: () => kbPaste(page),
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function pasteFromClipboard(page, selector = null) {
 | 
			
		||||
  const validator = e => e.clipboardData.items.length !== 0;
 | 
			
		||||
  await waitForEvent({
 | 
			
		||||
    page,
 | 
			
		||||
    eventName: "paste",
 | 
			
		||||
    action: () => kbPaste(page),
 | 
			
		||||
    selector,
 | 
			
		||||
    validator,
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getSerialized(page, filter = undefined) {
 | 
			
		||||
  const values = await page.evaluate(() => {
 | 
			
		||||
    const { map } =
 | 
			
		||||
      window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
 | 
			
		||||
    if (!map) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    const vals = Array.from(map.values());
 | 
			
		||||
    for (const value of vals) {
 | 
			
		||||
      for (const [k, v] of Object.entries(value)) {
 | 
			
		||||
        // Puppeteer don't serialize typed array correctly, so we convert them
 | 
			
		||||
        // to arrays.
 | 
			
		||||
        if (ArrayBuffer.isView(v)) {
 | 
			
		||||
          value[k] = Array.from(v);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return vals;
 | 
			
		||||
  });
 | 
			
		||||
  return filter ? values.map(filter) : values;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getFirstSerialized(page, filter = undefined) {
 | 
			
		||||
  return (await getSerialized(page, filter))[0];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getAnnotationStorage(page) {
 | 
			
		||||
  return page.evaluate(() =>
 | 
			
		||||
    Object.fromEntries(
 | 
			
		||||
      window.PDFViewerApplication.pdfDocument.annotationStorage.serializable
 | 
			
		||||
        .map || []
 | 
			
		||||
    )
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForEntryInStorage(page, key, value, checker = (x, y) => x === y) {
 | 
			
		||||
  return page.waitForFunction(
 | 
			
		||||
    (k, v, c) => {
 | 
			
		||||
      const { map } =
 | 
			
		||||
        window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
 | 
			
		||||
      // eslint-disable-next-line no-eval
 | 
			
		||||
      return map && eval(`(${c})`)(JSON.stringify(map.get(k)), v);
 | 
			
		||||
    },
 | 
			
		||||
    {},
 | 
			
		||||
    key,
 | 
			
		||||
    JSON.stringify(value),
 | 
			
		||||
    checker.toString()
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getEditors(page, kind) {
 | 
			
		||||
  return page.evaluate(aKind => {
 | 
			
		||||
    const elements = document.querySelectorAll(`.${aKind}Editor`);
 | 
			
		||||
    const results = [];
 | 
			
		||||
    for (const { id } of elements) {
 | 
			
		||||
      results.push(parseInt(id.split("_").at(-1)));
 | 
			
		||||
    }
 | 
			
		||||
    results.sort();
 | 
			
		||||
    return results;
 | 
			
		||||
  }, kind);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function getEditorDimensions(page, selector) {
 | 
			
		||||
  return page.evaluate(sel => {
 | 
			
		||||
    const { style } = document.querySelector(sel);
 | 
			
		||||
    return {
 | 
			
		||||
      left: style.left,
 | 
			
		||||
      top: style.top,
 | 
			
		||||
      width: style.width,
 | 
			
		||||
      height: style.height,
 | 
			
		||||
    };
 | 
			
		||||
  }, selector);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function serializeBitmapDimensions(page) {
 | 
			
		||||
  await page.waitForFunction(() => {
 | 
			
		||||
    try {
 | 
			
		||||
      const map =
 | 
			
		||||
        window.PDFViewerApplication.pdfDocument.annotationStorage.serializable
 | 
			
		||||
          .map;
 | 
			
		||||
      return !!map;
 | 
			
		||||
    } catch {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return page.evaluate(() => {
 | 
			
		||||
    const { map } =
 | 
			
		||||
      window.PDFViewerApplication.pdfDocument.annotationStorage.serializable;
 | 
			
		||||
    return map
 | 
			
		||||
      ? Array.from(map.values(), x => ({
 | 
			
		||||
          width: x.bitmap.width,
 | 
			
		||||
          height: x.bitmap.height,
 | 
			
		||||
        }))
 | 
			
		||||
      : [];
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function dragAndDrop(page, selector, translations, steps = 1) {
 | 
			
		||||
  const rect = await getRect(page, selector);
 | 
			
		||||
  const startX = rect.x + rect.width / 2;
 | 
			
		||||
  const startY = rect.y + rect.height / 2;
 | 
			
		||||
  await page.mouse.move(startX, startY);
 | 
			
		||||
  await page.mouse.down();
 | 
			
		||||
  for (const [tX, tY] of translations) {
 | 
			
		||||
    await page.mouse.move(startX + tX, startY + tY, { steps });
 | 
			
		||||
  }
 | 
			
		||||
  await page.mouse.up();
 | 
			
		||||
  await page.waitForSelector("#viewer:not(.noUserSelect)");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForPageChanging(page) {
 | 
			
		||||
  return createPromise(page, resolve => {
 | 
			
		||||
    window.PDFViewerApplication.eventBus.on("pagechanging", resolve, {
 | 
			
		||||
      once: true,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForAnnotationEditorLayer(page) {
 | 
			
		||||
  return createPromise(page, resolve => {
 | 
			
		||||
    window.PDFViewerApplication.eventBus.on(
 | 
			
		||||
      "annotationeditorlayerrendered",
 | 
			
		||||
      resolve,
 | 
			
		||||
      { once: true }
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForAnnotationModeChanged(page) {
 | 
			
		||||
  return createPromise(page, resolve => {
 | 
			
		||||
    window.PDFViewerApplication.eventBus.on(
 | 
			
		||||
      "annotationeditormodechanged",
 | 
			
		||||
      resolve,
 | 
			
		||||
      { once: true }
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForPageRendered(page, pageNumber) {
 | 
			
		||||
  return page.evaluateHandle(
 | 
			
		||||
    number => [
 | 
			
		||||
      new Promise(resolve => {
 | 
			
		||||
        const { eventBus } = window.PDFViewerApplication;
 | 
			
		||||
        eventBus.on("pagerendered", function handler(e) {
 | 
			
		||||
          if (
 | 
			
		||||
            !e.isDetailView &&
 | 
			
		||||
            (number === undefined || e.pageNumber === number)
 | 
			
		||||
          ) {
 | 
			
		||||
            resolve();
 | 
			
		||||
            eventBus.off("pagerendered", handler);
 | 
			
		||||
          }
 | 
			
		||||
        });
 | 
			
		||||
      }),
 | 
			
		||||
    ],
 | 
			
		||||
    pageNumber
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForEditorMovedInDOM(page) {
 | 
			
		||||
  return createPromise(page, resolve => {
 | 
			
		||||
    window.PDFViewerApplication.eventBus.on("editormovedindom", resolve, {
 | 
			
		||||
      once: true,
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function scrollIntoView(page, selector) {
 | 
			
		||||
  const handle = await page.evaluateHandle(
 | 
			
		||||
    sel => [
 | 
			
		||||
      new Promise(resolve => {
 | 
			
		||||
        const container = document.getElementById("viewerContainer");
 | 
			
		||||
        if (container.scrollHeight <= container.clientHeight) {
 | 
			
		||||
          resolve();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        container.addEventListener("scrollend", resolve, { once: true });
 | 
			
		||||
        const element = document.querySelector(sel);
 | 
			
		||||
        element.scrollIntoView({ behavior: "instant", block: "start" });
 | 
			
		||||
      }),
 | 
			
		||||
    ],
 | 
			
		||||
    selector
 | 
			
		||||
  );
 | 
			
		||||
  return awaitPromise(handle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function firstPageOnTop(page) {
 | 
			
		||||
  const handle = await page.evaluateHandle(() => [
 | 
			
		||||
    new Promise(resolve => {
 | 
			
		||||
      const container = document.getElementById("viewerContainer");
 | 
			
		||||
      if (container.scrollTop === 0 && container.scrollLeft === 0) {
 | 
			
		||||
        resolve();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      container.addEventListener("scrollend", resolve, { once: true });
 | 
			
		||||
      container.scrollTo(0, 0);
 | 
			
		||||
    }),
 | 
			
		||||
  ]);
 | 
			
		||||
  return awaitPromise(handle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function setCaretAt(page, pageNumber, text, position) {
 | 
			
		||||
  await page.evaluate(
 | 
			
		||||
    (pageN, string, pos) => {
 | 
			
		||||
      for (const el of document.querySelectorAll(
 | 
			
		||||
        `.page[data-page-number="${pageN}"] > .textLayer > span`
 | 
			
		||||
      )) {
 | 
			
		||||
        if (el.textContent === string) {
 | 
			
		||||
          window.getSelection().setPosition(el.firstChild, pos);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    },
 | 
			
		||||
    pageNumber,
 | 
			
		||||
    text,
 | 
			
		||||
    position
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const modifier = isMac ? "Meta" : "Control";
 | 
			
		||||
async function kbCopy(page) {
 | 
			
		||||
  await page.keyboard.down(modifier);
 | 
			
		||||
  await page.keyboard.press("c", { commands: ["Copy"] });
 | 
			
		||||
  await page.keyboard.up(modifier);
 | 
			
		||||
}
 | 
			
		||||
async function kbPaste(page) {
 | 
			
		||||
  await page.keyboard.down(modifier);
 | 
			
		||||
  await page.keyboard.press("v", { commands: ["Paste"] });
 | 
			
		||||
  await page.keyboard.up(modifier);
 | 
			
		||||
}
 | 
			
		||||
async function kbUndo(page) {
 | 
			
		||||
  await page.keyboard.down(modifier);
 | 
			
		||||
  await page.keyboard.press("z");
 | 
			
		||||
  await page.keyboard.up(modifier);
 | 
			
		||||
}
 | 
			
		||||
async function kbRedo(page) {
 | 
			
		||||
  if (isMac) {
 | 
			
		||||
    await page.keyboard.down("Meta");
 | 
			
		||||
    await page.keyboard.down("Shift");
 | 
			
		||||
    await page.keyboard.press("z");
 | 
			
		||||
    await page.keyboard.up("Shift");
 | 
			
		||||
    await page.keyboard.up("Meta");
 | 
			
		||||
  } else {
 | 
			
		||||
    await page.keyboard.down("Control");
 | 
			
		||||
    await page.keyboard.press("y");
 | 
			
		||||
    await page.keyboard.up("Control");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
async function kbSelectAll(page) {
 | 
			
		||||
  await page.keyboard.down(modifier);
 | 
			
		||||
  await page.keyboard.press("a", { commands: ["SelectAll"] });
 | 
			
		||||
  await page.keyboard.up(modifier);
 | 
			
		||||
}
 | 
			
		||||
async function kbModifierDown(page) {
 | 
			
		||||
  await page.keyboard.down(modifier);
 | 
			
		||||
}
 | 
			
		||||
async function kbModifierUp(page) {
 | 
			
		||||
  await page.keyboard.up(modifier);
 | 
			
		||||
}
 | 
			
		||||
async function kbGoToEnd(page) {
 | 
			
		||||
  if (isMac) {
 | 
			
		||||
    await page.keyboard.down("Meta");
 | 
			
		||||
    await page.keyboard.press("ArrowDown", {
 | 
			
		||||
      commands: ["MoveToEndOfDocument"],
 | 
			
		||||
    });
 | 
			
		||||
    await page.keyboard.up("Meta");
 | 
			
		||||
  } else {
 | 
			
		||||
    await page.keyboard.down("Control");
 | 
			
		||||
    await page.keyboard.press("End");
 | 
			
		||||
    await page.keyboard.up("Control");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
async function kbGoToBegin(page) {
 | 
			
		||||
  if (isMac) {
 | 
			
		||||
    await page.keyboard.down("Meta");
 | 
			
		||||
    await page.keyboard.press("ArrowUp", {
 | 
			
		||||
      commands: ["MoveToBeginningOfDocument"],
 | 
			
		||||
    });
 | 
			
		||||
    await page.keyboard.up("Meta");
 | 
			
		||||
  } else {
 | 
			
		||||
    await page.keyboard.down("Control");
 | 
			
		||||
    await page.keyboard.press("Home");
 | 
			
		||||
    await page.keyboard.up("Control");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
async function kbBigMoveLeft(page) {
 | 
			
		||||
  if (isMac) {
 | 
			
		||||
    await page.keyboard.down("Shift");
 | 
			
		||||
    await page.keyboard.press("ArrowLeft");
 | 
			
		||||
    await page.keyboard.up("Shift");
 | 
			
		||||
  } else {
 | 
			
		||||
    await page.keyboard.down("Control");
 | 
			
		||||
    await page.keyboard.press("ArrowLeft");
 | 
			
		||||
    await page.keyboard.up("Control");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
async function kbBigMoveRight(page) {
 | 
			
		||||
  if (isMac) {
 | 
			
		||||
    await page.keyboard.down("Shift");
 | 
			
		||||
    await page.keyboard.press("ArrowRight");
 | 
			
		||||
    await page.keyboard.up("Shift");
 | 
			
		||||
  } else {
 | 
			
		||||
    await page.keyboard.down("Control");
 | 
			
		||||
    await page.keyboard.press("ArrowRight");
 | 
			
		||||
    await page.keyboard.up("Control");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
async function kbBigMoveUp(page) {
 | 
			
		||||
  if (isMac) {
 | 
			
		||||
    await page.keyboard.down("Shift");
 | 
			
		||||
    await page.keyboard.press("ArrowUp");
 | 
			
		||||
    await page.keyboard.up("Shift");
 | 
			
		||||
  } else {
 | 
			
		||||
    await page.keyboard.down("Control");
 | 
			
		||||
    await page.keyboard.press("ArrowUp");
 | 
			
		||||
    await page.keyboard.up("Control");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
async function kbBigMoveDown(page) {
 | 
			
		||||
  if (isMac) {
 | 
			
		||||
    await page.keyboard.down("Shift");
 | 
			
		||||
    await page.keyboard.press("ArrowDown");
 | 
			
		||||
    await page.keyboard.up("Shift");
 | 
			
		||||
  } else {
 | 
			
		||||
    await page.keyboard.down("Control");
 | 
			
		||||
    await page.keyboard.press("ArrowDown");
 | 
			
		||||
    await page.keyboard.up("Control");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function kbDeleteLastWord(page) {
 | 
			
		||||
  if (isMac) {
 | 
			
		||||
    await page.keyboard.down("Alt");
 | 
			
		||||
    await page.keyboard.press("Backspace");
 | 
			
		||||
    await page.keyboard.up("Alt");
 | 
			
		||||
  } else {
 | 
			
		||||
    await page.keyboard.down("Control");
 | 
			
		||||
    await page.keyboard.press("Backspace");
 | 
			
		||||
    await page.keyboard.up("Control");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function kbFocusNext(page) {
 | 
			
		||||
  const handle = await createPromise(page, resolve => {
 | 
			
		||||
    window.addEventListener("focusin", resolve, { once: true });
 | 
			
		||||
  });
 | 
			
		||||
  await page.keyboard.press("Tab");
 | 
			
		||||
  await awaitPromise(handle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function kbFocusPrevious(page) {
 | 
			
		||||
  const handle = await createPromise(page, resolve => {
 | 
			
		||||
    window.addEventListener("focusin", resolve, { once: true });
 | 
			
		||||
  });
 | 
			
		||||
  await page.keyboard.down("Shift");
 | 
			
		||||
  await page.keyboard.press("Tab");
 | 
			
		||||
  await page.keyboard.up("Shift");
 | 
			
		||||
  await awaitPromise(handle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function kbSave(page) {
 | 
			
		||||
  await page.keyboard.down(modifier);
 | 
			
		||||
  await page.keyboard.press("s");
 | 
			
		||||
  await page.keyboard.up(modifier);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function switchToEditor(name, page, disable = false) {
 | 
			
		||||
  const modeChangedHandle = await createPromise(page, resolve => {
 | 
			
		||||
    window.PDFViewerApplication.eventBus.on(
 | 
			
		||||
      "annotationeditormodechanged",
 | 
			
		||||
      resolve,
 | 
			
		||||
      { once: true }
 | 
			
		||||
    );
 | 
			
		||||
  });
 | 
			
		||||
  await page.click(`#editor${name}Button`);
 | 
			
		||||
  name = name.toLowerCase();
 | 
			
		||||
  await page.waitForSelector(
 | 
			
		||||
    ".annotationEditorLayer" +
 | 
			
		||||
      (disable ? `:not(.${name}Editing)` : `.${name}Editing`)
 | 
			
		||||
  );
 | 
			
		||||
  await awaitPromise(modeChangedHandle);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function selectEditors(name, page) {
 | 
			
		||||
  await kbSelectAll(page);
 | 
			
		||||
  await page.waitForFunction(
 | 
			
		||||
    () => !document.querySelector(`.${name}Editor:not(.selectedEditor)`)
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function clearEditors(name, page) {
 | 
			
		||||
  await selectEditors(name, page);
 | 
			
		||||
  await page.keyboard.press("Backspace");
 | 
			
		||||
  await waitForStorageEntries(page, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForNoElement(page, selector) {
 | 
			
		||||
  return page.waitForFunction(
 | 
			
		||||
    sel => !document.querySelector(sel),
 | 
			
		||||
    {},
 | 
			
		||||
    selector
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function isCanvasMonochrome(page, pageNumber, rectangle, color) {
 | 
			
		||||
  return page.evaluate(
 | 
			
		||||
    (rect, pageN, col) => {
 | 
			
		||||
      const canvas = document.querySelector(
 | 
			
		||||
        `.page[data-page-number = "${pageN}"] .canvasWrapper canvas`
 | 
			
		||||
      );
 | 
			
		||||
      const canvasRect = canvas.getBoundingClientRect();
 | 
			
		||||
      const ctx = canvas.getContext("2d");
 | 
			
		||||
      rect ||= canvasRect;
 | 
			
		||||
      const { data } = ctx.getImageData(
 | 
			
		||||
        rect.x - canvasRect.x,
 | 
			
		||||
        rect.y - canvasRect.y,
 | 
			
		||||
        rect.width,
 | 
			
		||||
        rect.height
 | 
			
		||||
      );
 | 
			
		||||
      return new Uint32Array(data.buffer).every(x => x === col);
 | 
			
		||||
    },
 | 
			
		||||
    rectangle,
 | 
			
		||||
    pageNumber,
 | 
			
		||||
    color
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function getXY(page, selector) {
 | 
			
		||||
  const rect = await getRect(page, selector);
 | 
			
		||||
  return `${rect.x}::${rect.y}`;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function waitForPositionChange(page, selector, xy) {
 | 
			
		||||
  return page.waitForFunction(
 | 
			
		||||
    (sel, currentXY) => {
 | 
			
		||||
      const bbox = document.querySelector(sel).getBoundingClientRect();
 | 
			
		||||
      return `${bbox.x}::${bbox.y}` !== currentXY;
 | 
			
		||||
    },
 | 
			
		||||
    {},
 | 
			
		||||
    selector,
 | 
			
		||||
    xy
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
async function moveEditor(page, selector, n, pressKey) {
 | 
			
		||||
  let xy = await getXY(page, selector);
 | 
			
		||||
  for (let i = 0; i < n; i++) {
 | 
			
		||||
    const handle = await waitForEditorMovedInDOM(page);
 | 
			
		||||
    await pressKey();
 | 
			
		||||
    await awaitPromise(handle);
 | 
			
		||||
    await waitForPositionChange(page, selector, xy);
 | 
			
		||||
    xy = await getXY(page, selector);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Unicode bidi isolation characters, Fluent adds these markers to the text.
 | 
			
		||||
const FSI = "\u2068";
 | 
			
		||||
const PDI = "\u2069";
 | 
			
		||||
 | 
			
		||||
export {
 | 
			
		||||
  applyFunctionToEditor,
 | 
			
		||||
  awaitPromise,
 | 
			
		||||
  clearEditors,
 | 
			
		||||
  clearInput,
 | 
			
		||||
  closePages,
 | 
			
		||||
  closeSinglePage,
 | 
			
		||||
  copy,
 | 
			
		||||
  copyToClipboard,
 | 
			
		||||
  createPromise,
 | 
			
		||||
  dragAndDrop,
 | 
			
		||||
  firstPageOnTop,
 | 
			
		||||
  FSI,
 | 
			
		||||
  getAnnotationSelector,
 | 
			
		||||
  getAnnotationStorage,
 | 
			
		||||
  getComputedStyleSelector,
 | 
			
		||||
  getEditorDimensions,
 | 
			
		||||
  getEditors,
 | 
			
		||||
  getEditorSelector,
 | 
			
		||||
  getFirstSerialized,
 | 
			
		||||
  getQuerySelector,
 | 
			
		||||
  getRect,
 | 
			
		||||
  getSelector,
 | 
			
		||||
  getSerialized,
 | 
			
		||||
  getSpanRectFromText,
 | 
			
		||||
  getXY,
 | 
			
		||||
  isCanvasMonochrome,
 | 
			
		||||
  kbBigMoveDown,
 | 
			
		||||
  kbBigMoveLeft,
 | 
			
		||||
  kbBigMoveRight,
 | 
			
		||||
  kbBigMoveUp,
 | 
			
		||||
  kbDeleteLastWord,
 | 
			
		||||
  kbFocusNext,
 | 
			
		||||
  kbFocusPrevious,
 | 
			
		||||
  kbGoToBegin,
 | 
			
		||||
  kbGoToEnd,
 | 
			
		||||
  kbModifierDown,
 | 
			
		||||
  kbModifierUp,
 | 
			
		||||
  kbRedo,
 | 
			
		||||
  kbSave,
 | 
			
		||||
  kbSelectAll,
 | 
			
		||||
  kbUndo,
 | 
			
		||||
  loadAndWait,
 | 
			
		||||
  mockClipboard,
 | 
			
		||||
  moveEditor,
 | 
			
		||||
  paste,
 | 
			
		||||
  pasteFromClipboard,
 | 
			
		||||
  PDI,
 | 
			
		||||
  scrollIntoView,
 | 
			
		||||
  selectEditor,
 | 
			
		||||
  selectEditors,
 | 
			
		||||
  serializeBitmapDimensions,
 | 
			
		||||
  setCaretAt,
 | 
			
		||||
  switchToEditor,
 | 
			
		||||
  unselectEditor,
 | 
			
		||||
  waitAndClick,
 | 
			
		||||
  waitForAnnotationEditorLayer,
 | 
			
		||||
  waitForAnnotationModeChanged,
 | 
			
		||||
  waitForEntryInStorage,
 | 
			
		||||
  waitForEvent,
 | 
			
		||||
  waitForNoElement,
 | 
			
		||||
  waitForPageChanging,
 | 
			
		||||
  waitForPageRendered,
 | 
			
		||||
  waitForPointerUp,
 | 
			
		||||
  waitForSandboxTrip,
 | 
			
		||||
  waitForSelectedEditor,
 | 
			
		||||
  waitForSerialized,
 | 
			
		||||
  waitForStorageEntries,
 | 
			
		||||
  waitForTimeout,
 | 
			
		||||
  waitForUnselectedEditor,
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										39
									
								
								test/integration/text_field_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								test/integration/text_field_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
/* Copyright 2024 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 { closePages, getSelector, loadAndWait } from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
describe("Text field", () => {
 | 
			
		||||
  describe("Empty text field", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("file_pdfjs_form.pdf", getSelector("7R"));
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("must check that the field is empty although its appearance contains a white space", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          const text = await page.$eval(getSelector("7R"), el => el.value);
 | 
			
		||||
          expect(text).withContext(`In ${browserName}`).toEqual("");
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										571
									
								
								test/integration/text_layer_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										571
									
								
								test/integration/text_layer_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,571 @@
 | 
			
		||||
/* Copyright 2024 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 {
 | 
			
		||||
  closePages,
 | 
			
		||||
  closeSinglePage,
 | 
			
		||||
  getSpanRectFromText,
 | 
			
		||||
  loadAndWait,
 | 
			
		||||
  waitForEvent,
 | 
			
		||||
} from "./test_utils.mjs";
 | 
			
		||||
import { startBrowser } from "../test.mjs";
 | 
			
		||||
 | 
			
		||||
describe("Text layer", () => {
 | 
			
		||||
  describe("Text selection", () => {
 | 
			
		||||
    // page.mouse.move(x, y, { steps: ... }) doesn't work in Firefox, because
 | 
			
		||||
    // puppeteer will send fractional intermediate positions and Firefox doesn't
 | 
			
		||||
    // support them. Use this function to round each intermediate position to an
 | 
			
		||||
    // integer.
 | 
			
		||||
    async function moveInSteps(page, from, to, steps) {
 | 
			
		||||
      const deltaX = to.x - from.x;
 | 
			
		||||
      const deltaY = to.y - from.y;
 | 
			
		||||
      for (let i = 0; i <= steps; i++) {
 | 
			
		||||
        const x = Math.round(from.x + (deltaX * i) / steps);
 | 
			
		||||
        const y = Math.round(from.y + (deltaY * i) / steps);
 | 
			
		||||
        await page.mouse.move(x, y);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function middlePosition(rect) {
 | 
			
		||||
      return {
 | 
			
		||||
        x: Math.round(rect.x + rect.width / 2),
 | 
			
		||||
        y: Math.round(rect.y + rect.height / 2),
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function middleLeftPosition(rect) {
 | 
			
		||||
      return {
 | 
			
		||||
        x: Math.round(rect.x + 1),
 | 
			
		||||
        y: Math.round(rect.y + rect.height / 2),
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    function belowEndPosition(rect) {
 | 
			
		||||
      return {
 | 
			
		||||
        x: Math.round(rect.x + rect.width),
 | 
			
		||||
        y: Math.round(rect.y + rect.height * 1.5),
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
      jasmine.addAsyncMatchers({
 | 
			
		||||
        // Check that a page has a selection containing the given text, with
 | 
			
		||||
        // some tolerance for extra characters before/after.
 | 
			
		||||
        toHaveRoughlySelected({ pp }) {
 | 
			
		||||
          return {
 | 
			
		||||
            async compare(page, expected) {
 | 
			
		||||
              const TOLERANCE = 10;
 | 
			
		||||
 | 
			
		||||
              const actual = await page.evaluate(() =>
 | 
			
		||||
                // We need to normalize EOL for Windows
 | 
			
		||||
                window.getSelection().toString().replaceAll("\r\n", "\n")
 | 
			
		||||
              );
 | 
			
		||||
 | 
			
		||||
              let start, end;
 | 
			
		||||
              if (expected instanceof RegExp) {
 | 
			
		||||
                const match = expected.exec(actual);
 | 
			
		||||
                start = -1;
 | 
			
		||||
                if (match) {
 | 
			
		||||
                  start = match.index;
 | 
			
		||||
                  end = start + match[0].length;
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                start = actual.indexOf(expected);
 | 
			
		||||
                if (start !== -1) {
 | 
			
		||||
                  end = start + expected.length;
 | 
			
		||||
                }
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              const pass =
 | 
			
		||||
                start !== -1 &&
 | 
			
		||||
                start < TOLERANCE &&
 | 
			
		||||
                end > actual.length - TOLERANCE;
 | 
			
		||||
 | 
			
		||||
              return {
 | 
			
		||||
                pass,
 | 
			
		||||
                message: `Expected ${pp(
 | 
			
		||||
                  actual.length > 200
 | 
			
		||||
                    ? actual.slice(0, 100) + "[...]" + actual.slice(-100)
 | 
			
		||||
                    : actual
 | 
			
		||||
                )} to ${pass ? "not " : ""}roughly match ${pp(expected)}.`,
 | 
			
		||||
              };
 | 
			
		||||
            },
 | 
			
		||||
          };
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("using mouse", () => {
 | 
			
		||||
      describe("doesn't jump when hovering on an empty area", () => {
 | 
			
		||||
        let pages;
 | 
			
		||||
 | 
			
		||||
        beforeEach(async () => {
 | 
			
		||||
          pages = await loadAndWait(
 | 
			
		||||
            "tracemonkey.pdf",
 | 
			
		||||
            `.page[data-page-number = "1"] .endOfContent`
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        afterEach(async () => {
 | 
			
		||||
          await closePages(pages);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("in a single page", async () => {
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            pages.map(async ([browserName, page]) => {
 | 
			
		||||
              const [positionStart, positionEnd] = await Promise.all([
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  1,
 | 
			
		||||
                  "(frequently executed) bytecode sequences, records"
 | 
			
		||||
                ).then(middlePosition),
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  1,
 | 
			
		||||
                  "them, and compiles them to fast native code. We call such a se-"
 | 
			
		||||
                ).then(belowEndPosition),
 | 
			
		||||
              ]);
 | 
			
		||||
 | 
			
		||||
              await page.mouse.move(positionStart.x, positionStart.y);
 | 
			
		||||
              await page.mouse.down();
 | 
			
		||||
              await moveInSteps(page, positionStart, positionEnd, 20);
 | 
			
		||||
              await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
              await expectAsync(page)
 | 
			
		||||
                .withContext(`In ${browserName}`)
 | 
			
		||||
                .toHaveRoughlySelected(
 | 
			
		||||
                  "code sequences, records\n" +
 | 
			
		||||
                    "them, and compiles them to fast native code. We call suc"
 | 
			
		||||
                );
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("across multiple pages", async () => {
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            pages.map(async ([browserName, page]) => {
 | 
			
		||||
              const scrollTarget = await getSpanRectFromText(
 | 
			
		||||
                page,
 | 
			
		||||
                1,
 | 
			
		||||
                "Unlike method-based dynamic compilers, our dynamic com-"
 | 
			
		||||
              );
 | 
			
		||||
              await page.evaluate(top => {
 | 
			
		||||
                document.getElementById("viewerContainer").scrollTop = top;
 | 
			
		||||
              }, scrollTarget.y - 50);
 | 
			
		||||
 | 
			
		||||
              const [
 | 
			
		||||
                positionStartPage1,
 | 
			
		||||
                positionEndPage1,
 | 
			
		||||
                positionStartPage2,
 | 
			
		||||
                positionEndPage2,
 | 
			
		||||
              ] = await Promise.all([
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  1,
 | 
			
		||||
                  "Each compiled trace covers one path through the program with"
 | 
			
		||||
                ).then(middlePosition),
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  1,
 | 
			
		||||
                  "or that the same types will occur in subsequent loop iterations."
 | 
			
		||||
                ).then(middlePosition),
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  2,
 | 
			
		||||
                  "Hence, recording and compiling a trace"
 | 
			
		||||
                ).then(middlePosition),
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  2,
 | 
			
		||||
                  "cache. Alternatively, the VM could simply stop tracing, and give up"
 | 
			
		||||
                ).then(belowEndPosition),
 | 
			
		||||
              ]);
 | 
			
		||||
 | 
			
		||||
              await page.mouse.move(positionStartPage1.x, positionStartPage1.y);
 | 
			
		||||
              await page.mouse.down();
 | 
			
		||||
 | 
			
		||||
              await moveInSteps(page, positionStartPage1, positionEndPage1, 20);
 | 
			
		||||
              await moveInSteps(page, positionEndPage1, positionStartPage2, 20);
 | 
			
		||||
 | 
			
		||||
              await expectAsync(page)
 | 
			
		||||
                .withContext(`In ${browserName}, first selection`)
 | 
			
		||||
                .toHaveRoughlySelected(
 | 
			
		||||
                  /path through the program .*Hence, recording a/s
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
              await moveInSteps(page, positionStartPage2, positionEndPage2, 20);
 | 
			
		||||
              await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
              await expectAsync(page)
 | 
			
		||||
                .withContext(`In ${browserName}, second selection`)
 | 
			
		||||
                .toHaveRoughlySelected(
 | 
			
		||||
                  /path through.*Hence, recording and .* tracing, and give/s
 | 
			
		||||
                );
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      describe("doesn't jump when hovering on an empty area, with .markedContent", () => {
 | 
			
		||||
        let pages;
 | 
			
		||||
 | 
			
		||||
        beforeEach(async () => {
 | 
			
		||||
          pages = await loadAndWait(
 | 
			
		||||
            "chrome-text-selection-markedContent.pdf",
 | 
			
		||||
            `.page[data-page-number = "1"] .endOfContent`
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        afterEach(async () => {
 | 
			
		||||
          await closePages(pages);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("in per-character selection mode", async () => {
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            pages.map(async ([browserName, page]) => {
 | 
			
		||||
              const [positionStart, positionEnd] = await Promise.all([
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  1,
 | 
			
		||||
                  "strengthen in the coming quarters as the railway projects under"
 | 
			
		||||
                ).then(middlePosition),
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  1,
 | 
			
		||||
                  "development enter the construction phase (estimated at around"
 | 
			
		||||
                ).then(belowEndPosition),
 | 
			
		||||
              ]);
 | 
			
		||||
 | 
			
		||||
              await page.mouse.move(positionStart.x, positionStart.y);
 | 
			
		||||
              await page.mouse.down();
 | 
			
		||||
              await moveInSteps(page, positionStart, positionEnd, 20);
 | 
			
		||||
              await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
              await expectAsync(page)
 | 
			
		||||
                .withContext(`In ${browserName}`)
 | 
			
		||||
                .toHaveRoughlySelected(
 | 
			
		||||
                  "rs as the railway projects under\n" +
 | 
			
		||||
                    "development enter the construction phase (estimated at "
 | 
			
		||||
                );
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("in per-word selection mode", async () => {
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            pages.map(async ([browserName, page]) => {
 | 
			
		||||
              const [positionStart, positionEnd] = await Promise.all([
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  1,
 | 
			
		||||
                  "strengthen in the coming quarters as the railway projects under"
 | 
			
		||||
                ).then(middlePosition),
 | 
			
		||||
                getSpanRectFromText(
 | 
			
		||||
                  page,
 | 
			
		||||
                  1,
 | 
			
		||||
                  "development enter the construction phase (estimated at around"
 | 
			
		||||
                ).then(belowEndPosition),
 | 
			
		||||
              ]);
 | 
			
		||||
 | 
			
		||||
              // Puppeteer doesn't support emulating "double click and hold" for
 | 
			
		||||
              // WebDriver BiDi, so we must manually dispatch a protocol action
 | 
			
		||||
              // (see https://github.com/puppeteer/puppeteer/issues/13745).
 | 
			
		||||
              await page.mainFrame().browsingContext.performActions([
 | 
			
		||||
                {
 | 
			
		||||
                  type: "pointer",
 | 
			
		||||
                  id: "__puppeteer_mouse",
 | 
			
		||||
                  actions: [
 | 
			
		||||
                    { type: "pointerMove", ...positionStart },
 | 
			
		||||
                    { type: "pointerDown", button: 0 },
 | 
			
		||||
                    { type: "pointerUp", button: 0 },
 | 
			
		||||
                    { type: "pointerDown", button: 0 },
 | 
			
		||||
                  ],
 | 
			
		||||
                },
 | 
			
		||||
              ]);
 | 
			
		||||
              await moveInSteps(page, positionStart, positionEnd, 20);
 | 
			
		||||
              await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
              await expectAsync(page)
 | 
			
		||||
                .withContext(`In ${browserName}`)
 | 
			
		||||
                .toHaveRoughlySelected(
 | 
			
		||||
                  "quarters as the railway projects under\n" +
 | 
			
		||||
                    "development enter the construction phase (estimated at around"
 | 
			
		||||
                );
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      describe("when selecting over a link", () => {
 | 
			
		||||
        let pages;
 | 
			
		||||
 | 
			
		||||
        beforeEach(async () => {
 | 
			
		||||
          pages = await loadAndWait(
 | 
			
		||||
            "annotation-link-text-popup.pdf",
 | 
			
		||||
            `.page[data-page-number = "1"] .endOfContent`
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        afterEach(async () => {
 | 
			
		||||
          await closePages(pages);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("allows selecting within the link", async () => {
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            pages.map(async ([browserName, page]) => {
 | 
			
		||||
              const [positionStart, positionEnd] = await Promise.all([
 | 
			
		||||
                getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
 | 
			
		||||
                getSpanRectFromText(page, 1, "mozilla.org").then(
 | 
			
		||||
                  middlePosition
 | 
			
		||||
                ),
 | 
			
		||||
              ]);
 | 
			
		||||
 | 
			
		||||
              await page.mouse.move(positionStart.x, positionStart.y);
 | 
			
		||||
              await page.mouse.down();
 | 
			
		||||
              await moveInSteps(page, positionStart, positionEnd, 20);
 | 
			
		||||
              await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
              await expectAsync(page)
 | 
			
		||||
                .withContext(`In ${browserName}`)
 | 
			
		||||
                .toHaveRoughlySelected("Link\nmozil");
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("allows selecting within the link when going backwards", async () => {
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            pages.map(async ([browserName, page]) => {
 | 
			
		||||
              const [positionStart, positionEnd] = await Promise.all([
 | 
			
		||||
                getSpanRectFromText(page, 1, "Text").then(middlePosition),
 | 
			
		||||
                getSpanRectFromText(page, 1, "mozilla.org").then(
 | 
			
		||||
                  middlePosition
 | 
			
		||||
                ),
 | 
			
		||||
              ]);
 | 
			
		||||
 | 
			
		||||
              await page.mouse.move(positionStart.x, positionStart.y);
 | 
			
		||||
              await page.mouse.down();
 | 
			
		||||
              await moveInSteps(page, positionStart, positionEnd, 20);
 | 
			
		||||
              await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
              await expectAsync(page)
 | 
			
		||||
                .withContext(`In ${browserName}`)
 | 
			
		||||
                .toHaveRoughlySelected("a.org\nTe");
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("allows clicking the link after selecting", async () => {
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            pages.map(async ([browserName, page]) => {
 | 
			
		||||
              const [positionStart, positionEnd] = await Promise.all([
 | 
			
		||||
                getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
 | 
			
		||||
                getSpanRectFromText(page, 1, "mozilla.org").then(
 | 
			
		||||
                  middlePosition
 | 
			
		||||
                ),
 | 
			
		||||
              ]);
 | 
			
		||||
 | 
			
		||||
              await page.mouse.move(positionStart.x, positionStart.y);
 | 
			
		||||
              await page.mouse.down();
 | 
			
		||||
              await moveInSteps(page, positionStart, positionEnd, 20);
 | 
			
		||||
              await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
              await waitForEvent({
 | 
			
		||||
                page,
 | 
			
		||||
                eventName: "click",
 | 
			
		||||
                action: () => page.mouse.click(positionEnd.x, positionEnd.y),
 | 
			
		||||
                selector: "#pdfjs_internal_id_8R",
 | 
			
		||||
                validator: e => {
 | 
			
		||||
                  // Don't navigate to the link destination: the `click` event
 | 
			
		||||
                  // firing is enough validation that the link can be clicked.
 | 
			
		||||
                  e.preventDefault();
 | 
			
		||||
                  return true;
 | 
			
		||||
                },
 | 
			
		||||
              });
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        it("allows clicking the link after changing selection with the keyboard", async () => {
 | 
			
		||||
          await Promise.all(
 | 
			
		||||
            pages.map(async ([browserName, page]) => {
 | 
			
		||||
              const [positionStart, positionEnd] = await Promise.all([
 | 
			
		||||
                getSpanRectFromText(page, 1, "Link").then(middleLeftPosition),
 | 
			
		||||
                getSpanRectFromText(page, 1, "mozilla.org").then(
 | 
			
		||||
                  middlePosition
 | 
			
		||||
                ),
 | 
			
		||||
              ]);
 | 
			
		||||
 | 
			
		||||
              await page.mouse.move(positionStart.x, positionStart.y);
 | 
			
		||||
              await page.mouse.down();
 | 
			
		||||
              await moveInSteps(page, positionStart, positionEnd, 20);
 | 
			
		||||
              await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
              await page.keyboard.down("Shift");
 | 
			
		||||
              await page.keyboard.press("ArrowRight");
 | 
			
		||||
              await page.keyboard.up("Shift");
 | 
			
		||||
 | 
			
		||||
              await waitForEvent({
 | 
			
		||||
                page,
 | 
			
		||||
                eventName: "click",
 | 
			
		||||
                action: () => page.mouse.click(positionEnd.x, positionEnd.y),
 | 
			
		||||
                selector: "#pdfjs_internal_id_8R",
 | 
			
		||||
                validator: e => {
 | 
			
		||||
                  // Don't navigate to the link destination: the `click` event
 | 
			
		||||
                  // firing is enough validation that the link can be clicked.
 | 
			
		||||
                  e.preventDefault();
 | 
			
		||||
                  return true;
 | 
			
		||||
                },
 | 
			
		||||
              });
 | 
			
		||||
            })
 | 
			
		||||
          );
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe("using selection carets", () => {
 | 
			
		||||
      let browser;
 | 
			
		||||
      let page;
 | 
			
		||||
 | 
			
		||||
      beforeEach(async () => {
 | 
			
		||||
        // Chrome does not support simulating caret-based selection, so this
 | 
			
		||||
        // test only runs in Firefox.
 | 
			
		||||
        browser = await startBrowser({
 | 
			
		||||
          browserName: "firefox",
 | 
			
		||||
          startUrl: "",
 | 
			
		||||
          extraPrefsFirefox: {
 | 
			
		||||
            "layout.accessiblecaret.enabled": true,
 | 
			
		||||
            "layout.accessiblecaret.hide_carets_for_mouse_input": false,
 | 
			
		||||
          },
 | 
			
		||||
        });
 | 
			
		||||
        page = await browser.newPage();
 | 
			
		||||
        await page.goto(
 | 
			
		||||
          `${global.integrationBaseUrl}?file=/test/pdfs/tracemonkey.pdf#zoom=page-fit`
 | 
			
		||||
        );
 | 
			
		||||
        await page.bringToFront();
 | 
			
		||||
        await page.waitForSelector(
 | 
			
		||||
          `.page[data-page-number = "1"] .endOfContent`,
 | 
			
		||||
          { timeout: 0 }
 | 
			
		||||
        );
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      afterEach(async () => {
 | 
			
		||||
        await closeSinglePage(page);
 | 
			
		||||
        await browser.close();
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it("doesn't jump when moving selection", async () => {
 | 
			
		||||
        const [initialStart, initialEnd, finalEnd] = await Promise.all([
 | 
			
		||||
          getSpanRectFromText(
 | 
			
		||||
            page,
 | 
			
		||||
            1,
 | 
			
		||||
            "(frequently executed) bytecode sequences, records"
 | 
			
		||||
          ).then(middleLeftPosition),
 | 
			
		||||
          getSpanRectFromText(
 | 
			
		||||
            page,
 | 
			
		||||
            1,
 | 
			
		||||
            "(frequently executed) bytecode sequences, records"
 | 
			
		||||
          ).then(middlePosition),
 | 
			
		||||
          getSpanRectFromText(
 | 
			
		||||
            page,
 | 
			
		||||
            1,
 | 
			
		||||
            "them, and compiles them to fast native code. We call such a se-"
 | 
			
		||||
          ).then(belowEndPosition),
 | 
			
		||||
        ]);
 | 
			
		||||
 | 
			
		||||
        await page.mouse.move(initialStart.x, initialStart.y);
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await moveInSteps(page, initialStart, initialEnd, 20);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
        await expectAsync(page)
 | 
			
		||||
          .withContext(`first selection`)
 | 
			
		||||
          .toHaveRoughlySelected("frequently executed) byt");
 | 
			
		||||
 | 
			
		||||
        const initialCaretPos = {
 | 
			
		||||
          x: initialEnd.x,
 | 
			
		||||
          y: initialEnd.y + 10,
 | 
			
		||||
        };
 | 
			
		||||
        const intermediateCaretPos = {
 | 
			
		||||
          x: finalEnd.x,
 | 
			
		||||
          y: finalEnd.y + 5,
 | 
			
		||||
        };
 | 
			
		||||
        const finalCaretPos = {
 | 
			
		||||
          x: finalEnd.x + 20,
 | 
			
		||||
          y: finalEnd.y + 5,
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        await page.mouse.move(initialCaretPos.x, initialCaretPos.y);
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await moveInSteps(page, initialCaretPos, intermediateCaretPos, 20);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
        await expectAsync(page)
 | 
			
		||||
          .withContext(`second selection`)
 | 
			
		||||
          .toHaveRoughlySelected(/frequently .* We call such a s/s);
 | 
			
		||||
 | 
			
		||||
        await page.mouse.down();
 | 
			
		||||
        await moveInSteps(page, intermediateCaretPos, finalCaretPos, 20);
 | 
			
		||||
        await page.mouse.up();
 | 
			
		||||
 | 
			
		||||
        await expectAsync(page)
 | 
			
		||||
          .withContext(`third selection`)
 | 
			
		||||
          .toHaveRoughlySelected(/frequently .* We call such a s/s);
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe("when the browser enforces a minimum font size", () => {
 | 
			
		||||
    let browser;
 | 
			
		||||
    let page;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      // Only testing in Firefox because, while Chrome has a setting similar to
 | 
			
		||||
      // font.minimum-size.x-western, it is not exposed through its API.
 | 
			
		||||
      browser = await startBrowser({
 | 
			
		||||
        browserName: "firefox",
 | 
			
		||||
        startUrl: "",
 | 
			
		||||
        extraPrefsFirefox: { "font.minimum-size.x-western": 40 },
 | 
			
		||||
      });
 | 
			
		||||
      page = await browser.newPage();
 | 
			
		||||
      await page.goto(
 | 
			
		||||
        `${global.integrationBaseUrl}?file=/test/pdfs/tracemonkey.pdf#zoom=100`
 | 
			
		||||
      );
 | 
			
		||||
      await page.bringToFront();
 | 
			
		||||
      await page.waitForSelector(
 | 
			
		||||
        `.page[data-page-number = "1"] .endOfContent`,
 | 
			
		||||
        { timeout: 0 }
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closeSinglePage(page);
 | 
			
		||||
      await browser.close();
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("renders spans with the right size", async () => {
 | 
			
		||||
      const rect = await getSpanRectFromText(
 | 
			
		||||
        page,
 | 
			
		||||
        1,
 | 
			
		||||
        "Dynamic languages such as JavaScript are more difficult to com-"
 | 
			
		||||
      );
 | 
			
		||||
 | 
			
		||||
      // The difference between `a` and `b`, as a percentage of the lower one
 | 
			
		||||
      const getPercentDiff = (a, b) => Math.max(a, b) / Math.min(a, b) - 1;
 | 
			
		||||
 | 
			
		||||
      expect(getPercentDiff(rect.width, 315)).toBeLessThan(0.03);
 | 
			
		||||
      expect(getPercentDiff(rect.height, 12)).toBeLessThan(0.03);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										35
									
								
								test/integration/thumbnail_view_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								test/integration/thumbnail_view_spec.mjs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import { closePages, loadAndWait } from "./test_utils.mjs";
 | 
			
		||||
 | 
			
		||||
describe("PDF Thumbnail View", () => {
 | 
			
		||||
  describe("Works without errors", () => {
 | 
			
		||||
    let pages;
 | 
			
		||||
 | 
			
		||||
    beforeEach(async () => {
 | 
			
		||||
      pages = await loadAndWait("tracemonkey.pdf", "#sidebarToggleButton");
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    afterEach(async () => {
 | 
			
		||||
      await closePages(pages);
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    it("should render thumbnails without errors", async () => {
 | 
			
		||||
      await Promise.all(
 | 
			
		||||
        pages.map(async ([browserName, page]) => {
 | 
			
		||||
          await page.click("#sidebarToggleButton");
 | 
			
		||||
 | 
			
		||||
          const thumbSelector = "#thumbnailView .thumbnailImage";
 | 
			
		||||
          await page.waitForSelector(thumbSelector, { visible: true });
 | 
			
		||||
 | 
			
		||||
          await page.waitForSelector(
 | 
			
		||||
            '#thumbnailView .thumbnail[data-loaded="true"]'
 | 
			
		||||
          );
 | 
			
		||||
 | 
			
		||||
          const src = await page.$eval(thumbSelector, el => el.src);
 | 
			
		||||
          expect(src)
 | 
			
		||||
            .withContext(`In ${browserName}`)
 | 
			
		||||
            .toMatch(/^data:image\//);
 | 
			
		||||
        })
 | 
			
		||||
      );
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										1464
									
								
								test/integration/viewer_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1464
									
								
								test/integration/viewer_spec.mjs
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
		Reference in New Issue
	
	Block a user