chromium/chrome/test/data/pdf/gesture_detector_test.ts

// Copyright 2016 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 {PinchEventDetail} from 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/pdf_viewer_wrapper.js';
import {GestureDetector} from 'chrome-extension://mhjfbmdgcfjbbpaeojofohoefgiehjai/pdf_viewer_wrapper.js';

import {createWheelEvent} from './test_util.js';

chrome.test.runTests(function() {
  'use strict';

  class PinchListener {
    lastEvent: CustomEvent<PinchEventDetail>|null = null;

    constructor(gestureDetector: GestureDetector) {
      gestureDetector.getEventTarget().addEventListener(
          'pinchstart', e => this.onPinch_(e as CustomEvent<PinchEventDetail>));
      gestureDetector.getEventTarget().addEventListener(
          'pinchupdate',
          e => this.onPinch_(e as CustomEvent<PinchEventDetail>));
      gestureDetector.getEventTarget().addEventListener(
          'pinchend', e => this.onPinch_(e as CustomEvent<PinchEventDetail>));
    }

    private onPinch_(pinchEvent: CustomEvent<PinchEventDetail>) {
      this.lastEvent = pinchEvent;
    }
  }

  let stubElement: HTMLElement;

  function createStubElement(): HTMLElement {
    const stubElement = document.createElement('div');
    document.body.innerHTML = '';
    document.body.appendChild(stubElement);
    return stubElement;
  }

  function createTouchEvent(
      type: string, touches: Array<Partial<TouchInit>>): TouchEvent {
    return new TouchEvent(type, {
      touches: touches.map(t => {
        return new Touch(
            Object.assign({identifier: 0, target: stubElement}, t));
      }),
      // Necessary for preventDefault() to work.
      cancelable: true,
    });
  }

  return [
    function testTransformCenter() {
      stubElement = createStubElement();
      const gestureDetector = new GestureDetector(stubElement);
      const pinchListener = new PinchListener(gestureDetector);

      stubElement.style.position = 'absolute';
      stubElement.style.left = '1px';
      stubElement.style.top = '-1px';
      stubElement.dispatchEvent(
          createWheelEvent(1, {clientX: 2, clientY: 3}, true));
      chrome.test.assertEq('pinchupdate', pinchListener.lastEvent!.type);
      chrome.test.assertEq(
          {x: 1, y: 4}, pinchListener.lastEvent!.detail.center);

      chrome.test.succeed();
    },

    function testPinchZoomIn() {
      stubElement = createStubElement();
      const gestureDetector = new GestureDetector(stubElement);
      const pinchListener = new PinchListener(gestureDetector);

      stubElement.dispatchEvent(createTouchEvent('touchstart', [
        {clientX: 0, clientY: 0},
        {clientX: 0, clientY: 2},
      ]));
      chrome.test.assertEq('pinchstart', pinchListener.lastEvent!.type);
      chrome.test.assertEq(
          {center: {x: 0, y: 1}}, pinchListener.lastEvent!.detail);

      stubElement.dispatchEvent(createTouchEvent('touchmove', [
        {clientX: 0, clientY: 0},
        {clientX: 0, clientY: 4},
      ]));
      chrome.test.assertEq('pinchupdate', pinchListener.lastEvent!.type);
      chrome.test.assertEq(
          {
            scaleRatio: 2,
            direction: 'in',
            startScaleRatio: 2,
            center: {x: 0, y: 2},
          },
          pinchListener.lastEvent!.detail);

      stubElement.dispatchEvent(createTouchEvent('touchmove', [
        {clientX: 0, clientY: 0},
        {clientX: 0, clientY: 8},
      ]));
      chrome.test.assertEq('pinchupdate', pinchListener.lastEvent!.type);
      chrome.test.assertEq(
          {
            scaleRatio: 2,
            direction: 'in',
            startScaleRatio: 4,
            center: {x: 0, y: 4},
          },
          pinchListener.lastEvent!.detail);

      stubElement.dispatchEvent(createTouchEvent('touchend', []));
      chrome.test.assertEq('pinchend', pinchListener.lastEvent!.type);
      chrome.test.assertEq(
          {startScaleRatio: 4, center: {x: 0, y: 4}},
          pinchListener.lastEvent!.detail);

      chrome.test.succeed();
    },

    function testPinchZoomInAndBackOut() {
      stubElement = createStubElement();
      const gestureDetector = new GestureDetector(stubElement);
      const pinchListener = new PinchListener(gestureDetector);

      stubElement.dispatchEvent(createTouchEvent('touchstart', [
        {clientX: 0, clientY: 0},
        {clientX: 0, clientY: 2},
      ]));
      let {type, detail} = pinchListener.lastEvent!;
      chrome.test.assertEq('pinchstart', type);
      chrome.test.assertEq({center: {x: 0, y: 1}}, detail);

      stubElement.dispatchEvent(createTouchEvent('touchmove', [
        {clientX: 0, clientY: 0},
        {clientX: 0, clientY: 4},
      ]));
      ({type, detail} = pinchListener.lastEvent!);
      chrome.test.assertEq('pinchupdate', type);
      chrome.test.assertEq(
          {
            scaleRatio: 2,
            direction: 'in',
            startScaleRatio: 2,
            center: {x: 0, y: 2},
          },
          detail);

      stubElement.dispatchEvent(createTouchEvent('touchmove', [
        {clientX: 0, clientY: 0},
        {clientX: 0, clientY: 2},
      ]));
      // This should be part of the same gesture as an update.
      // A change in direction should not end the gesture and start a new one.
      ({type, detail} = pinchListener.lastEvent!);
      chrome.test.assertEq('pinchupdate', type);
      chrome.test.assertEq(
          {
            scaleRatio: 0.5,
            direction: 'out',
            startScaleRatio: 1,
            center: {x: 0, y: 1},
          },
          detail);

      stubElement.dispatchEvent(createTouchEvent('touchend', []));
      ({type, detail} = pinchListener.lastEvent!);
      chrome.test.assertEq('pinchend', type);
      chrome.test.assertEq({startScaleRatio: 1, center: {x: 0, y: 1}}, detail);

      chrome.test.succeed();
    },

    async function testZoomWithWheel() {
      stubElement = createStubElement();
      const gestureDetector = new GestureDetector(stubElement);
      const pinchListener = new PinchListener(gestureDetector);

      // Since the wheel events that the GestureDetector receives are
      // individual updates without begin/end events, we need to make sure the
      // GestureDetector generates appropriate pinch begin/end events itself.
      class PinchSequenceListener {
        seenBegin: boolean = false;
        endPromise: Promise<void>;

        constructor(gestureDetector: GestureDetector) {
          gestureDetector.getEventTarget().addEventListener(
              'pinchstart', () => {
                this.seenBegin = true;
              });

          this.endPromise = new Promise<void>(function(resolve) {
            gestureDetector.getEventTarget().addEventListener(
                'pinchend', () => resolve());
          });
        }
      }
      const pinchSequenceListener = new PinchSequenceListener(gestureDetector);

      const scale = 1.23;
      const deltaY = -(100.0 * Math.log(scale));
      const position = {clientX: 12, clientY: 34};
      stubElement.dispatchEvent(createWheelEvent(deltaY, position, true));

      chrome.test.assertTrue(pinchSequenceListener.seenBegin);

      const {type, detail} = pinchListener.lastEvent!;
      chrome.test.assertEq('pinchupdate', type);
      chrome.test.assertTrue(detail.scaleRatio !== null);
      chrome.test.assertTrue(Math.abs(detail.scaleRatio! - scale) < 0.001);
      chrome.test.assertEq('in', detail.direction);
      chrome.test.assertTrue(detail.startScaleRatio !== null);
      chrome.test.assertTrue(Math.abs(detail.startScaleRatio! - scale) < 0.001);
      chrome.test.assertEq(
          {x: position.clientX, y: position.clientY}, detail.center);

      await pinchSequenceListener.endPromise;

      chrome.test.succeed();
    },

    function testIgnoreTouchScrolling() {
      stubElement = createStubElement();
      const gestureDetector = new GestureDetector(stubElement);
      const pinchListener = new PinchListener(gestureDetector);

      const touchScrollStartEvent = createTouchEvent('touchstart', [
        {clientX: 0, clientY: 0},
      ]);
      stubElement.dispatchEvent(touchScrollStartEvent);
      chrome.test.assertEq(null, pinchListener.lastEvent);
      chrome.test.assertFalse(touchScrollStartEvent.defaultPrevented);

      stubElement.dispatchEvent(createTouchEvent('touchmove', [
        {clientX: 0, clientY: 1},
      ]));
      chrome.test.assertEq(null, pinchListener.lastEvent);

      stubElement.dispatchEvent(createTouchEvent('touchend', []));
      chrome.test.assertEq(null, pinchListener.lastEvent);

      chrome.test.succeed();
    },

    function testIgnoreWheelScrolling() {
      stubElement = createStubElement();
      const gestureDetector = new GestureDetector(stubElement);
      const pinchListener = new PinchListener(gestureDetector);

      // A wheel event where ctrlKey is false does not indicate zooming.
      stubElement.dispatchEvent(
          createWheelEvent(1, {clientX: 0, clientY: 0}, false));
      chrome.test.assertEq(null, pinchListener.lastEvent);

      chrome.test.succeed();
    },

    function testPreventNativePinchZoom() {
      const touchAction =
          window.getComputedStyle(document.documentElement).touchAction;

      chrome.test.assertEq('pan-x pan-y', touchAction);

      chrome.test.succeed();
    },

    function testPreventNativeZoomFromWheel() {
      stubElement = createStubElement();
      const gestureDetector = new GestureDetector(stubElement);
      new PinchListener(gestureDetector);

      // We should not preventDefault a wheel event where ctrlKey is false as
      // that would prevent scrolling, not zooming.
      const scrollingWheelEvent =
          createWheelEvent(1, {clientX: 0, clientY: 0}, false);
      stubElement.dispatchEvent(scrollingWheelEvent);
      chrome.test.assertFalse(scrollingWheelEvent.defaultPrevented);

      const zoomingWheelEvent =
          createWheelEvent(1, {clientX: 0, clientY: 0}, true);
      stubElement.dispatchEvent(zoomingWheelEvent);
      chrome.test.assertTrue(zoomingWheelEvent.defaultPrevented);

      chrome.test.succeed();
    },

    function testWasTwoFingerTouch() {
      stubElement = createStubElement();
      const gestureDetector = new GestureDetector(stubElement);


      chrome.test.assertFalse(
          gestureDetector.wasTwoFingerTouch(),
          'Should not have two finger touch before first touch event.');

      stubElement.dispatchEvent(createTouchEvent('touchstart', [
        {clientX: 0, clientY: 0},
      ]));
      chrome.test.assertFalse(
          gestureDetector.wasTwoFingerTouch(),
          'Should not have a two finger touch with one touch.');

      stubElement.dispatchEvent(createTouchEvent('touchstart', [
        {clientX: 0, clientY: 0},
        {clientX: 2, clientY: 2},
      ]));
      chrome.test.assertTrue(
          gestureDetector.wasTwoFingerTouch(),
          'Should have a two finger touch.');

      // Make sure we keep |wasTwoFingerTouch| true after the end event.
      stubElement.dispatchEvent(createTouchEvent('touchend', []));
      chrome.test.assertTrue(
          gestureDetector.wasTwoFingerTouch(),
          'Should maintain two finger touch after touchend.');

      stubElement.dispatchEvent(createTouchEvent('touchstart', [
        {clientX: 0, clientY: 0},
        {clientX: 2, clientY: 2},
        {clientX: 4, clientY: 4},
      ]));
      chrome.test.assertFalse(
          gestureDetector.wasTwoFingerTouch(),
          'Should not have two finger touch with 3 touches.');

      chrome.test.succeed();
    },
  ];
}());