// Copyright 2018 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import type {AnnotationTool, ViewerInkHostElement} from 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/pdf_viewer_wrapper.js';
import {SaveRequestType} from 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/pdf_viewer_wrapper.js';
import {assert} from 'chrome://resources/js/assert.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {waitFor} from './test_util.js';
window.onerror = e => chrome.test.fail((e as unknown as Error).stack);
window.onunhandledrejection = e => chrome.test.fail(e.reason);
const viewer = document.body.querySelector('pdf-viewer')!;
function animationFrame(): Promise<void> {
return new Promise(resolve => requestAnimationFrame(() => resolve()));
}
function contentElement(): HTMLElement {
return viewer.shadowRoot!.elementFromPoint(innerWidth / 2, innerHeight / 2) as
HTMLElement;
}
function isAnnotationMode(): boolean {
return viewer.$.toolbar.annotationMode;
}
chrome.test.runTests([
function testAnnotationsEnabled() {
const toolbar = viewer.$.toolbar;
chrome.test.assertTrue(loadTimeData.getBoolean('pdfAnnotationsEnabled'));
chrome.test.assertTrue(
toolbar.shadowRoot!.querySelector('#annotate') != null);
chrome.test.succeed();
},
async function testEnterAnnotationMode() {
chrome.test.assertEq('EMBED', contentElement().tagName);
// Enter annotation mode.
viewer.$.toolbar.toggleAnnotation();
await viewer.loaded;
chrome.test.assertEq('VIEWER-INK-HOST', contentElement().tagName);
chrome.test.succeed();
},
async function testViewportToCameraConversion() {
chrome.test.assertTrue(isAnnotationMode());
const inkHost = contentElement() as ViewerInkHostElement;
const cameras: drawings.Box[] = [];
inkHost.getInkApiForTesting().setCamera = (camera: drawings.Box) => {
cameras.push(camera);
return Promise.resolve();
};
viewer.viewport.setZoom(1);
viewer.viewport.setZoom(2);
chrome.test.assertEq(2, cameras.length);
const scrollingContainer = viewer.$.scroller;
scrollingContainer.scrollTo(100, 100);
await animationFrame();
chrome.test.assertEq(3, cameras.length);
// When dark/light mode feature is enabled, a border will be applied to the
// window. See crrev.com/c/3656414 for more details.
const expectations = [
{
top: 2.25,
dark_light_top: 2.25,
left: -106.5,
dark_light_left: -105.75,
right: 718.5,
dark_light_right: 717.75,
bottom: -408.75,
dark_light_bottom: -408.0,
},
{
top: 2.25,
dark_light_top: 2.25,
left: -3.75,
dark_light_left: -3.75,
right: 408.75,
dark_light_right: 408,
bottom: -203.25,
dark_light_bottom: -202.875,
},
{
top: -35.25,
dark_light_top: -35.25,
left: 33.75,
dark_light_left: 33.75,
right: 446.25,
dark_light_right: 445.5,
bottom: -240.75,
dark_light_bottom: -240.375,
},
];
for (const expectation of expectations) {
const actual = cameras.shift()!;
chrome.test.assertTrue(
actual.top === expectation.top ||
actual.top === expectation.dark_light_top);
chrome.test.assertTrue(
actual.left === expectation.left ||
actual.left === expectation.dark_light_left);
chrome.test.assertTrue(
actual.bottom === expectation.bottom ||
actual.bottom === expectation.dark_light_bottom);
chrome.test.assertTrue(
actual.right === expectation.right ||
actual.right === expectation.dark_light_right);
}
chrome.test.succeed();
},
async function testPenOptions() {
chrome.test.assertTrue(isAnnotationMode());
const inkHost = contentElement() as ViewerInkHostElement;
let toolOrNull: AnnotationTool|null = null;
inkHost.getInkApiForTesting().setAnnotationTool =
(value: AnnotationTool) => {
toolOrNull = value;
};
// Pen defaults.
const viewerPdfToolbar = viewer.$.toolbar;
const viewerAnnotationsBar =
viewerPdfToolbar.shadowRoot!.querySelector('viewer-annotations-bar')!;
const pen = viewerAnnotationsBar.$.pen;
pen.click();
chrome.test.assertTrue(!!toolOrNull);
let tool = toolOrNull as AnnotationTool;
chrome.test.assertEq('pen', tool.tool);
chrome.test.assertEq(0.1429, tool.size);
chrome.test.assertEq('#000000', tool.color);
// Selected size and color.
const penOptions = viewerAnnotationsBar.shadowRoot!.querySelector(
'#pen viewer-pen-options')!;
penOptions.shadowRoot!.querySelector<HTMLElement>(
'#sizes [value="1"]')!.click();
penOptions.shadowRoot!
.querySelector<HTMLElement>('#colors [value="#00b0ff"]')!.click();
await animationFrame();
tool = toolOrNull as AnnotationTool;
chrome.test.assertEq('pen', tool.tool);
chrome.test.assertEq(1, tool.size);
chrome.test.assertEq('#00b0ff', tool.color);
// Eraser defaults.
viewerAnnotationsBar.$.eraser.click();
tool = toolOrNull as AnnotationTool;
chrome.test.assertEq('eraser', tool.tool);
chrome.test.assertEq(1, tool.size);
chrome.test.assertEq(null, tool.color);
// Pen keeps previous settings.
pen.click();
tool = toolOrNull as AnnotationTool;
chrome.test.assertEq('pen', tool.tool);
chrome.test.assertEq(1, tool.size);
chrome.test.assertEq('#00b0ff', tool.color);
// Highlighter defaults.
viewerAnnotationsBar.$.highlighter.click();
tool = toolOrNull as AnnotationTool;
chrome.test.assertEq('highlighter', tool.tool);
chrome.test.assertEq(0.7143, tool.size);
chrome.test.assertEq('#ffbc00', tool.color);
// Need to expand to use this color.
const highlighterOptions =
viewerAnnotationsBar.$.highlighter.querySelector('viewer-pen-options');
chrome.test.assertTrue(!!highlighterOptions);
viewerAnnotationsBar.$.highlighter.click();
const collapsedColor =
highlighterOptions.shadowRoot!.querySelector<HTMLInputElement>(
'#colors [value="#d1c4e9"]');
chrome.test.assertTrue(!!collapsedColor);
chrome.test.assertTrue(collapsedColor.disabled);
collapsedColor.click();
chrome.test.assertEq('#ffbc00', tool.color);
// Selected size and expanded color.
highlighterOptions.shadowRoot!
.querySelector<HTMLElement>('#sizes [value="1"]')!.click();
highlighterOptions.shadowRoot!
.querySelector<HTMLElement>('#colors #expand')!.click();
chrome.test.assertFalse(collapsedColor.disabled);
collapsedColor.click();
tool = toolOrNull as AnnotationTool;
chrome.test.assertEq('highlighter', tool.tool);
chrome.test.assertEq(1, tool.size);
chrome.test.assertEq('#d1c4e9', tool.color);
chrome.test.succeed();
},
async function testStrokeUndoRedo() {
const inkHost = contentElement();
const viewerPdfToolbar = viewer.$.toolbar;
const viewerAnnotationsBar =
viewerPdfToolbar.shadowRoot!.querySelector('viewer-annotations-bar')!;
const undo = viewerAnnotationsBar.$.undo;
const redo = viewerAnnotationsBar.$.redo;
const pen = {
pointerId: 2,
pointerType: 'pen',
pressure: 0.5,
clientX: inkHost.offsetWidth / 2,
clientY: inkHost.offsetHeight / 2,
buttons: 0,
};
// Initial state.
chrome.test.assertEq(undo.disabled, true);
chrome.test.assertEq(redo.disabled, true);
// Draw a stroke.
inkHost.dispatchEvent(new PointerEvent('pointerdown', pen));
inkHost.dispatchEvent(new PointerEvent('pointermove', pen));
inkHost.dispatchEvent(new PointerEvent('pointerup', pen));
await waitFor(() => undo.disabled === false);
chrome.test.assertEq(redo.disabled, true);
undo.click();
await waitFor(() => undo.disabled === true);
chrome.test.assertEq(redo.disabled, false);
redo.click();
await waitFor(() => undo.disabled === false);
chrome.test.assertEq(redo.disabled, true);
chrome.test.succeed();
},
async function testPointerEvents() {
chrome.test.assertTrue(isAnnotationMode());
const inkHost = contentElement() as ViewerInkHostElement;
inkHost.resetPenMode();
const events: PointerEvent[] = [];
inkHost.getInkApiForTesting().dispatchPointerEvent = (ev: PointerEvent) => {
events.push(ev);
};
const mouse = {pointerId: 1, pointerType: 'mouse', buttons: 1};
const pen = {
pointerId: 2,
pointerType: 'pen',
pressure: 0.5,
clientX: 3,
clientY: 4,
buttons: 0,
};
const touch1 = {pointerId: 11, pointerType: 'touch'};
const touch2 = {pointerId: 22, pointerType: 'touch'};
interface Expectation {
type: string;
init: PointerEventInit;
}
function checkExpectations(expectations: Expectation[]) {
chrome.test.assertEq(expectations.length, events.length);
while (expectations.length) {
const event = events.shift()!;
const expectation = expectations.shift()!;
chrome.test.assertEq(expectation.type, event.type);
interface IndexableType {
[key: string]: any;
}
for (const key of Object.keys(expectation.init)) {
chrome.test.assertEq(
(expectation.init as IndexableType)[key],
(event as IndexableType)[key]);
}
}
}
// Normal sequence.
inkHost.dispatchEvent(new PointerEvent('pointerdown', pen));
inkHost.dispatchEvent(new PointerEvent('pointermove', pen));
inkHost.dispatchEvent(new PointerEvent('pointerup', pen));
checkExpectations([
{type: 'pointerdown', init: pen},
{type: 'pointermove', init: pen},
{type: 'pointerup', init: pen},
]);
// Multi-touch gesture should cancel and suppress first pointer.
inkHost.resetPenMode();
inkHost.dispatchEvent(new PointerEvent('pointerdown', touch1));
inkHost.dispatchEvent(new PointerEvent('pointerdown', touch2));
inkHost.dispatchEvent(new PointerEvent('pointermove', touch1));
inkHost.dispatchEvent(new PointerEvent('pointerup', touch1));
checkExpectations([
{type: 'pointerdown', init: touch1},
{type: 'pointercancel', init: touch1},
]);
// Pointers which are not active should be suppressed.
inkHost.resetPenMode();
inkHost.dispatchEvent(new PointerEvent('pointerdown', mouse));
inkHost.dispatchEvent(new PointerEvent('pointerdown', pen));
inkHost.dispatchEvent(new PointerEvent('pointerdown', touch1));
inkHost.dispatchEvent(new PointerEvent('pointermove', mouse));
inkHost.dispatchEvent(new PointerEvent('pointermove', pen));
inkHost.dispatchEvent(new PointerEvent('pointermove', touch1));
inkHost.dispatchEvent(new PointerEvent('pointerup', mouse));
inkHost.dispatchEvent(new PointerEvent('pointermove', pen));
checkExpectations([
{type: 'pointerdown', init: mouse},
{type: 'pointermove', init: mouse},
{type: 'pointerup', init: mouse},
]);
// pointerleave should cause mouseup
inkHost.dispatchEvent(new PointerEvent('pointerdown', mouse));
inkHost.dispatchEvent(new PointerEvent('pointerleave', mouse));
checkExpectations([
{type: 'pointerdown', init: mouse},
{type: 'pointerup', init: mouse},
]);
// pointerleave does not apply to non-mouse pointers
inkHost.dispatchEvent(new PointerEvent('pointerdown', pen));
inkHost.dispatchEvent(new PointerEvent('pointerleave', pen));
inkHost.dispatchEvent(new PointerEvent('pointerup', pen));
checkExpectations([
{type: 'pointerdown', init: pen},
{type: 'pointerup', init: pen},
]);
// Browser will cancel touch on pen input
inkHost.resetPenMode();
inkHost.dispatchEvent(new PointerEvent('pointerdown', touch1));
inkHost.dispatchEvent(new PointerEvent('pointercancel', touch1));
inkHost.dispatchEvent(new PointerEvent('pointerdown', pen));
inkHost.dispatchEvent(new PointerEvent('pointerup', pen));
checkExpectations([
{type: 'pointerdown', init: touch1},
{type: 'pointercancel', init: touch1},
{type: 'pointerdown', init: pen},
{type: 'pointerup', init: pen},
]);
chrome.test.succeed();
},
async function testTouchPanGestures() {
// Ensure that we have an out-of-bounds area.
viewer.viewport.setZoom(0.5);
chrome.test.assertTrue(isAnnotationMode());
const inkHost = contentElement() as ViewerInkHostElement;
function dispatchPointerEvent(type: string, init: PointerEventInit) {
const pointerEvent = new PointerEvent(type, init);
inkHost.dispatchEvent(pointerEvent);
return pointerEvent;
}
function dispatchPointerAndTouchEvents(
type: string, init: PointerEventInit) {
const pointerEvent = dispatchPointerEvent(type, init);
let touchPrevented = false;
// Can't use a real TouchEvent here, since |timestamp| is a read-only
// property and can't be set to a desired value.
inkHost.onTouchStart({
timeStamp: pointerEvent.timeStamp,
preventDefault() {
touchPrevented = true;
},
} as unknown as TouchEvent);
return touchPrevented;
}
const pen = {
pointerId: 2,
pointerType: 'pen',
pressure: 0.5,
clientX: innerWidth / 2,
clientY: innerHeight / 2,
};
const outOfBoundsPen = {
pointerId: 2,
pointerType: 'pen',
pressure: 0.5,
clientX: 2,
clientY: 3,
};
const touch = {
pointerId: 3,
pointerType: 'touch',
pressure: 0.5,
clientX: innerWidth / 2,
clientY: innerHeight / 2,
};
const outOfBoundsTouch = {
pointerId: 4,
pointerType: 'touch',
pressure: 0.5,
clientX: 4,
clientY: 5,
};
inkHost.resetPenMode();
let prevented = dispatchPointerAndTouchEvents('pointerdown', touch);
dispatchPointerEvent('pointerup', touch);
chrome.test.assertTrue(
prevented, 'in document touch should prevent default');
prevented = dispatchPointerAndTouchEvents('pointerdown', outOfBoundsTouch);
dispatchPointerEvent('pointerup', outOfBoundsTouch);
chrome.test.assertFalse(
prevented, 'out of bounds touch should start gesture');
prevented = dispatchPointerAndTouchEvents('pointerdown', pen);
dispatchPointerEvent('pointerup', pen);
chrome.test.assertTrue(prevented, 'in document pen should prevent default');
prevented = dispatchPointerAndTouchEvents('pointerdown', outOfBoundsPen);
dispatchPointerEvent('pointerup', outOfBoundsPen);
chrome.test.assertFalse(
prevented, 'out of bounds pen should start gesture');
chrome.test.assertTrue(
inkHost.getPenModeForTesting(), 'pen input should switch to pen mode');
prevented = dispatchPointerAndTouchEvents('pointerdown', touch);
dispatchPointerEvent('pointerup', touch);
chrome.test.assertFalse(
prevented, 'in document touch in pen mode should start gesture');
chrome.test.succeed();
},
async function testExitAnnotationMode() {
chrome.test.assertTrue(isAnnotationMode());
// Exit annotation mode.
viewer.$.toolbar.toggleAnnotation();
await viewer.loaded;
chrome.test.assertEq('EMBED', contentElement().tagName);
chrome.test.succeed();
},
async function testSaveAfterAnnotationMode() {
const saveData = await viewer.getCurrentControllerForTesting()!.save(
SaveRequestType.EDITED);
assert(saveData);
chrome.test.assertTrue(saveData.editModeForTesting!);
chrome.test.succeed();
},
]);