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

// 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.

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

import {getTrustedHTML} from 'chrome://resources/js/static_types.js';
import type {CrCheckboxElement} from 'chrome://resources/cr_elements/cr_checkbox/cr_checkbox.js';
import {keyDownOn, keyUpOn, pressAndReleaseKeyOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {assertDeepEquals, assertEquals, assertFalse, assertTrue, assertLT, assertGT} 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-checkbox', function() {
  let checkbox: CrCheckboxElement;
  let innerCheckbox: HTMLElement;

  function waitOneCycle(): Promise<void> {
    return new Promise(res => {
      window.setTimeout(() => res());
    });
  }

  setup(function() {
    document.body.innerHTML = getTrustedHTML`
      <cr-checkbox>
        <div>label
          <a>link</a>
        </div>
      </cr-checkbox>
    `;

    checkbox = document.querySelector('cr-checkbox')!;
    const innerBox =
        checkbox.shadowRoot!.querySelector<HTMLElement>('#checkbox');
    assertTrue(!!innerBox);
    innerCheckbox = innerBox;
    assertNotChecked();
  });

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

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

  function assertDisabled() {
    assertTrue(checkbox.disabled);
    assertFalse(checkbox.hasAttribute('tabindex'));
    assertEquals('-1', innerCheckbox.getAttribute('tabindex'));
    assertTrue(checkbox.hasAttribute('disabled'));
    assertEquals('true', innerCheckbox.getAttribute('aria-disabled'));
    assertEquals('none', getComputedStyle(checkbox).pointerEvents);
  }

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

  function triggerKeyPressEvent(keyName: string, element?: HTMLElement) {
    pressAndReleaseKeyOn(element || innerCheckbox, 0, [], keyName);
  }

  // Test that the control is checked when the user taps on it (no movement
  // between pointerdown and pointerup).
  test('ToggleByMouse', async () => {
    let whenChanged = eventToPromise('change', checkbox);
    checkbox.click();
    await whenChanged;
    assertChecked();
    whenChanged = eventToPromise('change', checkbox);
    checkbox.click();
    await whenChanged;
    assertNotChecked();
  });

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

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

    checkbox.checked = false;
    await checkbox.updateComplete;
    assertNotChecked();

    // Wait 1 cycle to make sure change-event was not fired.
    return waitOneCycle();
  });

  test('Toggle checkbox button click', async () => {
    let whenChanged = eventToPromise('change', checkbox);
    innerCheckbox.click();
    await whenChanged;
    assertChecked();
    whenChanged = eventToPromise('change', checkbox);
    triggerKeyPressEvent('Enter');
    await whenChanged;
    assertNotChecked();
    whenChanged = eventToPromise('change', checkbox);
    triggerKeyPressEvent(' ');
    await whenChanged;
    assertChecked();
  });

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

    eventToPromise('change', checkbox).then(function() {
      assertFalse(true);
    });

    checkbox.click();
    await checkbox.updateComplete;
    assertNotChecked();

    innerCheckbox.click();
    await checkbox.updateComplete;
    assertNotChecked();

    triggerKeyPressEvent('Enter');
    await checkbox.updateComplete;
    assertNotChecked();

    triggerKeyPressEvent(' ');
    await checkbox.updateComplete;
    assertNotChecked();

    // Wait 1 cycle to make sure change-event was not fired.
    return waitOneCycle();
  });

  test('LabelDisplay_NoLabel', function() {
    const labelContainer = checkbox.$.labelContainer;

    // Test that there's actually a label that's more than just the padding.
    assertGT(labelContainer.offsetWidth, 20);

    checkbox.classList.add('no-label');
    assertEquals('none', getComputedStyle(labelContainer).display);
  });

  test('LabelDisplay_LabelFirst', () => {
    let checkboxRect = checkbox.$.checkbox.getBoundingClientRect();

    const labelContainer = checkbox.$.labelContainer;
    let labelContainerRect = labelContainer.getBoundingClientRect();

    assertLT(checkboxRect.left, labelContainerRect.left);

    checkbox.classList.add('label-first');
    checkboxRect = checkbox.$.checkbox.getBoundingClientRect();
    labelContainerRect = labelContainer.getBoundingClientRect();
    assertGT(checkboxRect.left, labelContainerRect.left);
  });

  test('ClickedOnLinkDoesNotToggleCheckbox', async () => {
    eventToPromise('change', checkbox).then(() => {
      assertFalse(true);
    });

    assertNotChecked();
    const link = document.querySelector('a')!;
    link.click();
    await checkbox.updateComplete;
    assertNotChecked();

    triggerKeyPressEvent('Enter', link);
    await checkbox.updateComplete;
    assertNotChecked();

    // Wait 1 cycle to make sure change-event was not fired.
    return waitOneCycle();
  });

  test('space key down does not toggle', async () => {
    assertNotChecked();
    keyDownOn(innerCheckbox, 0, [], ' ');
    await checkbox.updateComplete;
    assertNotChecked();
  });

  test('space key up toggles', async () => {
    assertNotChecked();
    keyUpOn(innerCheckbox, 0, [], ' ');
    await checkbox.updateComplete;
    assertChecked();
  });

  test('InitializingWithTabindex', function() {
    document.body.innerHTML = getTrustedHTML`
      <cr-checkbox id="checkbox" tab-index="-1"></cr-checkbox>
    `;

    checkbox = document.querySelector('cr-checkbox')!;
    innerCheckbox = checkbox.$.checkbox;

    // Should not override tabindex if it is initialized.
    assertEquals(-1, checkbox.tabIndex);
    assertFalse(checkbox.hasAttribute('tabindex'));
    assertEquals('-1', innerCheckbox.getAttribute('tabindex'));
  });

  test('InitializingWithDisabled', function() {
    document.body.innerHTML = getTrustedHTML`
      <cr-checkbox id="checkbox" disabled></cr-checkbox>
    `;

    checkbox = document.querySelector('cr-checkbox')!;
    innerCheckbox = checkbox.$.checkbox;

    // Initializing with disabled should make tabindex="-1".
    assertEquals(-1, checkbox.tabIndex);
    assertFalse(checkbox.hasAttribute('tabindex'));
    assertEquals('-1', innerCheckbox.getAttribute('tabindex'));
  });

  test('tabindex attribute is controlled by tabIndex', () => {
    document.body.innerHTML = getTrustedHTML`
      <cr-checkbox id="checkbox" tabindex="-1"></cr-checkbox>
    `;
    checkbox = document.querySelector('cr-checkbox')!;
    assertEquals(0, checkbox.tabIndex);
    assertFalse(checkbox.hasAttribute('tabindex'));
    assertEquals('0', innerCheckbox.getAttribute('tabindex'));
  });

  // 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-checkbox checked="{{parentChecked}}"
              on-change="onChange"
              on-checked-changed="onCheckedChanged">
          </cr-checkbox>`;
      }

      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);
      }

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

        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 checkbox = element.shadowRoot!.querySelector('cr-checkbox');
    assertTrue(!!checkbox);
    checkbox.click();
  });
});