chromium/chrome/test/data/webui/cr_elements/cr_toggle_test.ts

// Copyright 2017 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

// clang-format off
import 'chrome://resources/cr_elements/cr_toggle/cr_toggle.js';

import {getTrustedHTML} from 'chrome://resources/js/static_types.js';
import type {CrToggleElement} from 'chrome://resources/cr_elements/cr_toggle/cr_toggle.js';
import {MOVE_THRESHOLD_PX} from 'chrome://resources/cr_elements/cr_toggle/cr_toggle.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {eventToPromise} from 'chrome://webui-test/test_util.js';
import {html, PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
// clang-format on

suite('cr-toggle', function() {
  let toggle: CrToggleElement;

  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    toggle = document.createElement('cr-toggle');
    document.body.appendChild(toggle);
    assertNotChecked();
  });

  function assertChecked() {
    assertTrue(toggle.checked);
    assertTrue(toggle.hasAttribute('checked'));
    assertEquals('true', toggle.getAttribute('aria-pressed'));
  }

  function assertNotChecked() {
    assertFalse(toggle.checked);
    assertEquals(null, toggle.getAttribute('checked'));
    assertEquals('false', toggle.getAttribute('aria-pressed'));
  }

  function assertDisabled() {
    assertTrue(toggle.disabled);
    assertEquals('-1', toggle.getAttribute('tabindex'));
    assertTrue(toggle.hasAttribute('disabled'));
    assertEquals('true', toggle.getAttribute('aria-disabled'));
  }

  function assertNotDisabled() {
    assertFalse(toggle.disabled);
    assertEquals('0', toggle.getAttribute('tabindex'));
    assertFalse(toggle.hasAttribute('disabled'));
    assertEquals('false', toggle.getAttribute('aria-disabled'));
  }

  /**
   * Simulates dragging the toggle button left/right.
   * @param moveDirection -1 for left, 1 for right, 0 when no pointermove event
   *     should be simulated.
   * @param diff The move amount in pixels. Only relevant if moveDirection is
   *     non-zero.
   */
  function triggerPointerDownMoveUpTapSequence(
      moveDirection: -1|0|1, diff?: number) {
    const computedStyles = window.getComputedStyle(toggle);
    if (computedStyles.getPropertyValue('pointer-events') === 'none') {
      return;
    }

    // Simulate events in the same order they are fired by the browser.
    // Need to provide a valid |pointerId| for setPointerCapture() to not throw
    // an error.
    const xStart = 100;
    toggle.dispatchEvent(
        new PointerEvent('pointerdown', {pointerId: 1, clientX: xStart}));
    let xEnd = xStart;
    if (moveDirection) {
      xEnd = moveDirection > 0 ? xStart + diff! : xStart - diff!;
      toggle.dispatchEvent(
          new PointerEvent('pointermove', {pointerId: 1, clientX: xEnd}));
    }
    toggle.dispatchEvent(
        new PointerEvent('pointerup', {pointerId: 1, clientX: xEnd}));
    toggle.click();
  }

  // Check if setting checked in HTML works, has to use a separate element to
  // ensure that we are testing brand new state.
  test('initiallyCheckedWorks', function() {
    document.body.innerHTML = getTrustedHTML`<cr-toggle checked></cr-toggle> `;
    toggle = (document.querySelector('cr-toggle'))!;
    assertChecked();
  });

  // Test that the control is toggled when the |checked| attribute is
  // programmatically changed.
  test('ToggleByAttribute', async function() {
    eventToPromise('change', toggle).then(function() {
      // Should not fire 'change' event when state is changed programmatically.
      // Only user interaction should result in 'change' event.
      assertFalse(true);
    });

    toggle.checked = true;
    await toggle.updateComplete;
    assertChecked();

    toggle.checked = false;
    await toggle.updateComplete;
    assertNotChecked();
  });

  // Test that the control is toggled when the user taps on it (no movement
  // between pointerdown and pointerup).
  test('ToggleByPointerTap', async function() {
    let whenChanged = eventToPromise('change', toggle);
    triggerPointerDownMoveUpTapSequence(0 /* no pointermove */);
    await whenChanged;
    assertChecked();
    whenChanged = eventToPromise('change', toggle);
    triggerPointerDownMoveUpTapSequence(0 /* no pointermove */);
    await whenChanged;
    assertNotChecked();
  });

  // Test that the control is toggled if the user moves the pointer by a
  // MOVE_THRESHOLD_PX pixels accidentally (shaky hands) in any direction.
  test('ToggleByShakyPointerTap', async function() {
    let whenChanged = eventToPromise('change', toggle);
    triggerPointerDownMoveUpTapSequence(1 /* right */, MOVE_THRESHOLD_PX - 1);
    await whenChanged;
    assertChecked();
    whenChanged = eventToPromise('change', toggle);
    triggerPointerDownMoveUpTapSequence(1 /* right */, MOVE_THRESHOLD_PX - 1);
    await whenChanged;
    assertNotChecked();
  });

  // Test that the control is toggled when the user moves the pointer while
  // holding down.
  test('ToggleByPointerMove', async function() {
    let whenChanged = eventToPromise('change', toggle);
    triggerPointerDownMoveUpTapSequence(1 /* right */, MOVE_THRESHOLD_PX);
    await whenChanged;
    assertChecked();
    whenChanged = eventToPromise('change', toggle);
    triggerPointerDownMoveUpTapSequence(-1 /* left */, MOVE_THRESHOLD_PX);
    await whenChanged;
    assertNotChecked();
    whenChanged = eventToPromise('change', toggle);

    // Test simple tapping after having dragged.
    triggerPointerDownMoveUpTapSequence(0 /* no pointermove */);
    await whenChanged;
    assertChecked();
  });

  // Test that the control is toggled when the user presses the 'Enter' or
  // 'Space' key.
  test('ToggleByKey', async () => {
    assertNotChecked();

    toggle.dispatchEvent(
        new KeyboardEvent('keydown', {key: 'Enter', repeat: true}));
    await toggle.updateComplete;
    assertNotChecked();

    toggle.dispatchEvent(new KeyboardEvent('keydown', {key: ' '}));
    await toggle.updateComplete;
    assertNotChecked();

    toggle.dispatchEvent(
        new KeyboardEvent('keydown', {key: ' ', repeat: true}));
    await toggle.updateComplete;
    assertNotChecked();

    toggle.dispatchEvent(new KeyboardEvent('keydown', {key: 'Enter'}));
    await toggle.updateComplete;
    assertChecked();

    toggle.dispatchEvent(new KeyboardEvent('keyup', {key: ' '}));
    await toggle.updateComplete;
    assertNotChecked();
  });

  // Test that the control is not affected by user interaction when disabled.
  test('ToggleWhenDisabled', async function() {
    assertNotDisabled();
    toggle.disabled = true;
    await toggle.updateComplete;
    assertDisabled();

    triggerPointerDownMoveUpTapSequence(0 /* no pointermove */);
    await toggle.updateComplete;
    assertNotChecked();
    assertDisabled();

    toggle.disabled = false;
    await toggle.updateComplete;
    triggerPointerDownMoveUpTapSequence(0 /* no pointermove */);
    await toggle.updateComplete;
    assertChecked();
  });

  // Test that the control works as expected when the click() method is called.
  test('ToggleWhenWithClick', async function() {
    assertNotDisabled();
    assertNotChecked();

    // State should change because control is enabled.
    toggle.click();
    await toggle.updateComplete;
    assertChecked();

    // State should *not* change because control is disabled.
    toggle.disabled = true;
    await toggle.updateComplete;
    assertDisabled();

    toggle.click();
    await toggle.updateComplete;
    assertChecked();
  });

  // Test that 2-way bindings with Polymer parent elements are updated before
  // the 'change' event is fired.
  test('TwoWayBindingWithPolymerParent', function(done) {
    class TestElement extends PolymerElement {
      static get is() {
        return 'test-element';
      }

      static get template() {
        return html`
          <cr-toggle checked="{{parentChecked}}"
              on-change="onChange"
              on-checked-changed="onCheckedChanged">
          </cr-toggle>`;
      }

      static get properties() {
        return {
          parentChecked: Boolean,
        };
      }

      parentChecked: boolean = false;
      private events_: string[] = [];

      onCheckedChanged(e: CustomEvent<{value: boolean}>) {
        assertEquals(this.events_.length === 0 ? false : true, e.detail.value);
        this.events_.push(e.type);
        this.checkIfFinished_();
      }

      onChange(e: CustomEvent<boolean>) {
        assertTrue(e.detail);
        assertEquals(e.detail, element.parentChecked);
        this.events_.push(e.type);
        this.checkIfFinished_();
      }

      private checkIfFinished_() {
        if (this.events_.length !== 3) {
          return;
        }

        assertDeepEquals(
            ['checked-changed', 'checked-changed', 'change'], this.events_);
        done();
      }
    }

    customElements.define(TestElement.is, TestElement);

    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    const element = document.createElement('test-element') as TestElement;
    document.body.appendChild(element);

    const toggle = element.shadowRoot!.querySelector('cr-toggle');
    assertTrue(!!toggle);
    toggle.click();
  });

  test('cssPositionOfKnob', async () => {
    // Disable transitions for tests on the #knob to test accurate pixels.
    toggle.$.knob.style.transition = 'none';

    // Distance between center of knob and left edge of toggle should be
    // --cr-toggle-knob-center-edge-distance_ (8).
    let toggleBounds = toggle.getBoundingClientRect();
    let knobBounds = toggle.$.knob.getBoundingClientRect();
    let knobCenterDistance =
        (knobBounds.left + knobBounds.width / 2) - toggleBounds.left;
    assertEquals(8, knobCenterDistance);

    toggle.click();
    await toggle.updateComplete;

    // Distance between center of knob and right edge of toggle should be
    // --cr-toggle-knob-center-edge-distance_ (8).
    toggleBounds = toggle.getBoundingClientRect();
    knobBounds = toggle.$.knob.getBoundingClientRect();
    knobCenterDistance =
        toggleBounds.right - (knobBounds.left + knobBounds.width / 2);
    assertEquals(8, knobCenterDistance);
  });
});