chromium/third_party/google-closure-library/closure/goog/events/pastehandler_test.js

/**
 * @license
 * Copyright The Closure Library Authors.
 * SPDX-License-Identifier: Apache-2.0
 */

goog.module('goog.events.PasteHandlerTest');
goog.setTestOnly();

const BrowserEvent = goog.require('goog.events.BrowserEvent');
const EventType = goog.require('goog.events.EventType');
const GoogEventTarget = goog.require('goog.events.EventTarget');
const KeyCodes = goog.require('goog.events.KeyCodes');
const MockClock = goog.require('goog.testing.MockClock');
const PasteHandler = goog.require('goog.events.PasteHandler');
const dom = goog.require('goog.dom');
const events = goog.require('goog.events');
const testSuite = goog.require('goog.testing.testSuite');

/** @suppress {checkTypes} suppression added to enable type checking */
function newBrowserEvent(type) {
  if (typeof type === 'string') {
    return new BrowserEvent({type: type});
  } else {
    return new BrowserEvent(type);
  }
}

let textarea;
let clock;
let handler;
let pasted;

testSuite({
  setUp() {
    textarea = new GoogEventTarget();
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = '';
    clock = new MockClock(true);
    /** @suppress {checkTypes} suppression added to enable type checking */
    handler = new PasteHandler(textarea);
    pasted = false;
    events.listen(handler, PasteHandler.EventType.PASTE, () => {
      pasted = true;
    });
  },

  tearDown() {
    textarea.dispose();
    handler.dispose();
    clock.dispose();
  },

  testDispatchingPasteEventSupportedByAFewBrowsersWork() {
    if (!PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    /** @suppress {checkTypes} suppression added to enable type checking */
    const handlerThatSupportsPasteEvents = new PasteHandler(textarea);
    // user clicks on the textarea and give it focus
    events.listen(
        handlerThatSupportsPasteEvents, PasteHandler.EventType.PASTE, () => {
          pasted = true;
        });
    textarea.dispatchEvent(newBrowserEvent('paste'));
    assertTrue(pasted);
  },

  testJustTypingDoesntFirePasteEvent() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    // user clicks on the textarea and give it focus
    textarea.dispatchEvent(newBrowserEvent(EventType.FOCUS));
    assertFalse(pasted);
    // user starts typing
    textarea.dispatchEvent(newBrowserEvent({
      type: EventType.KEYDOWN,
      keyCode: KeyCodes.A,
    }));
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'a';
    assertFalse(pasted);

    // still typing
    textarea.dispatchEvent({type: EventType.KEYDOWN, keyCode: KeyCodes.B});
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'ab';
    assertFalse(pasted);

    // ends typing
    textarea.dispatchEvent({type: EventType.KEYDOWN, keyCode: KeyCodes.C});
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'abc';
    assertFalse(pasted);
  },

  testStartsOnInitialState() {
    assertTrue(handler.getState() == PasteHandler.State.INIT);
    assertFalse(pasted);
  },

  testBlurOnInit() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    textarea.dispatchEvent(EventType.BLUR);
    assertTrue(handler.getState() == PasteHandler.State.INIT);
    assertFalse(pasted);
  },

  testFocusOnInit() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    textarea.dispatchEvent(EventType.FOCUS);
    assertTrue(handler.getState() == PasteHandler.State.FOCUSED);
    assertFalse(pasted);
  },

  testInputOnFocus() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    // user clicks on the textarea
    textarea.dispatchEvent(newBrowserEvent(EventType.FOCUS));
    clock.tick(PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER + 1);
    // and right click -> paste a text!
    textarea.dispatchEvent(newBrowserEvent('input'));
    assertTrue(handler.getState() == PasteHandler.State.FOCUSED);
    // make sure we detected it
    assertTrue(pasted);
  },

  testKeyPressOnFocus() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    // user clicks on the textarea
    textarea.dispatchEvent(newBrowserEvent(EventType.FOCUS));

    // starts typing something
    textarea.dispatchEvent(newBrowserEvent({
      type: EventType.KEYDOWN,
      keyCode: KeyCodes.A,
    }));
    assertTrue(handler.getState() == PasteHandler.State.TYPING);
    assertFalse(pasted);

    // and then presses ctrl+v
    textarea.dispatchEvent(newBrowserEvent({
      type: EventType.KEYDOWN,
      keyCode: KeyCodes.V,
      ctrlKey: true,
    }));
    assertTrue(handler.getState() == PasteHandler.State.TYPING);

    // makes sure we detected it
    assertTrue(pasted);
  },

  testMouseOverOnInit() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    // user has something on the events
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'pasted string';
    // and right click -> paste it on the textarea, WITHOUT giving focus
    textarea.dispatchEvent(newBrowserEvent(EventType.MOUSEOVER));
    assertTrue(handler.getState() == PasteHandler.State.INIT);
    // makes sure we detect it
    assertTrue(pasted);

    pasted = false;

    // user normaly mouseovers the textarea, with no text change
    textarea.dispatchEvent(EventType.MOUSEOVER);
    assertTrue(handler.getState() == PasteHandler.State.INIT);
    // text area value doesn't change
    assertFalse(pasted);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testMouseOverAfterTyping() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    textarea.dispatchEvent(EventType.FOCUS);
    assertFalse(pasted);
    textarea.dispatchEvent({type: EventType.KEYDOWN, keyCode: KeyCodes.A});
    assertFalse(pasted);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'a';
    textarea.dispatchEvent('input');
    assertFalse(pasted);
    assertEquals('a', handler.oldValue_);
    textarea.dispatchEvent(EventType.MOUSEOVER);
    assertFalse(pasted);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testTypingAndThenRightClickPaste() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    textarea.dispatchEvent(EventType.FOCUS);

    textarea.dispatchEvent({type: EventType.KEYDOWN, keyCode: KeyCodes.A});
    assertFalse(pasted);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'a';
    clock.tick(PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER + 1);
    textarea.dispatchEvent('input');
    assertFalse(pasted);

    assertEquals('a', handler.oldValue_);

    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'ab';
    clock.tick(PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER + 1);
    textarea.dispatchEvent(newBrowserEvent('input'));
    assertTrue(pasted);
  },

  testTypingReallyFastDispatchesTwoInputEventsBeforeTheKeyDownEvent() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    textarea.dispatchEvent(EventType.FOCUS);

    // keydown and input events seems to be fired indepently: even though input
    // should happen after the key event, it doesn't if the user types fast
    // enough. FF2 + linux doesn't fire keydown events for every key pressed
    // when you type fast enough. if one of the keydown events gets swallowed,
    // two input events are fired consecutively. notice that there is a similar
    // scenario, that actually does produce a valid paste action.
    // {@see testRightClickRightClickAlsoDispatchesTwoConsecutiveInputEvents}

    textarea.dispatchEvent({type: EventType.KEYDOWN, keyCode: KeyCodes.A});
    assertFalse(pasted);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'a';
    clock.tick(PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER - 1);
    textarea.dispatchEvent('input');
    assertFalse(pasted);

    // second key down events gets fired on a different order
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'ab';
    clock.tick(PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER - 1);
    textarea.dispatchEvent('input');
    assertFalse(pasted);
  },

  testRightClickRightClickAlsoDispatchesTwoConsecutiveInputEvents() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    textarea.dispatchEvent(EventType.FOCUS);

    // there is also another case that two consecutive INPUT events are fired,
    // but in a valid paste action: if the user edit -> paste -> edit -> paste,
    // it is a valid paste action.

    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'a';
    clock.tick(PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER + 1);
    textarea.dispatchEvent(newBrowserEvent('input'));
    assertTrue(pasted);

    // second key down events gets fired on a different order
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'ab';
    clock.tick(PasteHandler.MANDATORY_MS_BETWEEN_INPUT_EVENTS_TIE_BREAKER + 1);
    textarea.dispatchEvent(newBrowserEvent('input'));
    assertTrue(pasted);
  },

  testMiddleClickWithoutFocusTriggersPasteEvent() {
    if (PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    // if the textarea is NOT selected, and then we use the middle button,
    // FF2+linux pastes what was last highlighted, causing a paste action.
    textarea.dispatchEvent(EventType.FOCUS);
    textarea.dispatchEvent(newBrowserEvent('input'));
    assertTrue(pasted);
  },

  testScriptingDoesntTriggerPasteEvents() {
    /** @suppress {checkTypes} suppression added to enable type checking */
    const handlerUsedToListenForScriptingChanges = new PasteHandler(textarea);
    pasted = false;
    // user clicks on the textarea and give it focus
    events.listen(
        handlerUsedToListenForScriptingChanges, PasteHandler.EventType.PASTE,
        () => {
          pasted = true;
        });
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    dom.getElement('foo').value = 'dear paste handler,';
    assertFalse(pasted);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    dom.getElement('foo').value = 'please dont misunderstand script changes';
    assertFalse(pasted);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    dom.getElement('foo').value = 'with user generated paste events';
    assertFalse(pasted);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    dom.getElement('foo').value = 'thanks!';
    assertFalse(pasted);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testAfterPaste() {
    if (!PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    /** @suppress {checkTypes} suppression added to enable type checking */
    const handlerThatSupportsPasteEvents = new PasteHandler(textarea);
    pasted = false;
    events.listen(
        handlerThatSupportsPasteEvents, PasteHandler.EventType.PASTE, () => {
          pasted = true;
        });
    let afterPasteFired = false;
    events.listen(
        handlerThatSupportsPasteEvents, PasteHandler.EventType.AFTER_PASTE,
        () => {
          afterPasteFired = true;
        });

    // Initial paste event comes before AFTER_PASTE has fired.
    textarea.dispatchEvent(newBrowserEvent('paste'));
    assertTrue(pasted);
    assertFalse(afterPasteFired);

    // Once text is pasted, it takes a bit to detect it, at which point
    // AFTER_PASTE is fired.
    clock.tick(PasteHandler.PASTE_POLLING_PERIOD_MS_);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'text';
    clock.tick(PasteHandler.PASTE_POLLING_PERIOD_MS_);
    assertTrue(afterPasteFired);
  },

  /** @suppress {visibility} suppression added to enable type checking */
  testAfterPasteNotFiredIfDelayTooLong() {
    if (!PasteHandler.SUPPORTS_NATIVE_PASTE_EVENT) {
      return;
    }
    /** @suppress {checkTypes} suppression added to enable type checking */
    const handlerThatSupportsPasteEvents = new PasteHandler(textarea);
    pasted = false;
    events.listen(
        handlerThatSupportsPasteEvents, PasteHandler.EventType.PASTE, () => {
          pasted = true;
        });
    let afterPasteFired = false;
    events.listen(
        handlerThatSupportsPasteEvents, PasteHandler.EventType.AFTER_PASTE,
        () => {
          afterPasteFired = true;
        });

    // Initial paste event comes before AFTER_PASTE has fired.
    textarea.dispatchEvent(newBrowserEvent('paste'));
    assertTrue(pasted);
    assertFalse(afterPasteFired);

    // If the new text doesn't show up in time, we never fire AFTER_PASTE.
    clock.tick(PasteHandler.PASTE_POLLING_TIMEOUT_MS_);
    /**
     * @suppress {strictMissingProperties} suppression added to enable type
     * checking
     */
    textarea.value = 'text';
    clock.tick(PasteHandler.PASTE_POLLING_PERIOD_MS_);
    assertFalse(afterPasteFired);
  },
});