chromium/chrome/test/data/webui/chromeos/diagnostics/input_list_test.ts

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

import 'chrome://diagnostics/input_list.js';
import 'chrome://diagnostics/strings.m.js';
import 'chrome://webui-test/chromeos/mojo_webui_test_support.js';

import {DiagnosticsBrowserProxyImpl} from 'chrome://diagnostics/diagnostics_browser_proxy.js';
import {NavigationView} from 'chrome://diagnostics/diagnostics_types.js';
import {fakeKeyboards, fakeTouchDevices} from 'chrome://diagnostics/fake_data.js';
import {FakeInputDataProvider} from 'chrome://diagnostics/fake_input_data_provider.js';
import {ConnectionType, KeyboardInfo, MechanicalLayout, NumberPadPresence, PhysicalLayout, TopRightKey, TopRowKey} from 'chrome://diagnostics/input.mojom-webui.js';
import {InputCardElement} from 'chrome://diagnostics/input_card.js';
import {TouchDeviceInfo, TouchDeviceType} from 'chrome://diagnostics/input_data_provider.mojom-webui.js';
import {InputListElement} from 'chrome://diagnostics/input_list.js';
import {KeyboardTesterElement} from 'chrome://diagnostics/keyboard_tester.js';
import {setInputDataProviderForTesting} from 'chrome://diagnostics/mojo_interface_provider.js';
import {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import {loadTimeData} from 'chrome://resources/ash/common/load_time_data.m.js';
import {strictQuery} from 'chrome://resources/ash/common/typescript_utils/strict_query.js';
import {assert} from 'chrome://resources/js/assert.js';
import {PromiseResolver} from 'chrome://resources/js/promise_resolver.js';
import {assertArrayEquals, assertDeepEquals, assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chromeos/chai_assert.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';
import {eventToPromise, isVisible} from 'chrome://webui-test/test_util.js';

import {TestDiagnosticsBrowserProxy} from './test_diagnostics_browser_proxy.js';

suite('inputListTestSuite', function() {
  let inputListElement: InputListElement|null = null;

  const provider = new FakeInputDataProvider();

  const diagnosticsBrowserProxy = new TestDiagnosticsBrowserProxy();


  suiteSetup(() => {
    setInputDataProviderForTesting(provider);

    DiagnosticsBrowserProxyImpl.setInstance(diagnosticsBrowserProxy);
  });

  setup(() => {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;

    provider.setStartTesterWithClamshellMode();
    provider.setStartWithLidOpen();
  });

  teardown(() => {
    inputListElement?.remove();
    inputListElement = null;
    provider.reset();
  });

  function initializeInputList(
      keyboards = fakeKeyboards, touchDevices = fakeTouchDevices) {
    assertFalse(!!inputListElement);
    provider.setFakeConnectedDevices(keyboards, touchDevices);

    // Add the input list to the DOM.
    inputListElement = document.createElement('input-list');
    assert(inputListElement);
    document.body.appendChild(inputListElement);

    return flushTasks();
  }

  function getCardByDeviceType(deviceType: string): InputCardElement|null {
    assert(inputListElement);
    return inputListElement.shadowRoot!.querySelector<InputCardElement>(
        `input-card[device-type="${deviceType}"]`);
  }

  /**
   * openTouchscreenTester is a helper function for some boilerplate code. It
   * clicks the test button of a touchscreen, then clicks the start testing
   * button and makes sure canvas dialog is open.
   * @returns The touchscreenTester.
   */
  async function openTouchscreenTester(
      touchscreensToInitialize: TouchDeviceInfo[], isClamshellMode = true) {
    await initializeInputList([], touchscreensToInitialize);
    if (isClamshellMode) {
      provider.setStartTesterWithClamshellMode();
    } else {
      provider.setStartTesterWithTabletMode();
    }

    const resolver = new PromiseResolver();
    assert(inputListElement);
    const touchscreenTester =
        inputListElement!.shadowRoot!.querySelector('touchscreen-tester');
    assert(touchscreenTester);
    const introDialog =
        touchscreenTester.getDialog('intro-dialog') as CrDialogElement;

    // Mock requestFullscreen function since this API can only be initiated by a
    // user gesture.
    introDialog.requestFullscreen = (): any => {
      resolver.resolve(undefined);
    };

    const testButton = strictQuery(
        'cr-button', getCardByDeviceType('touchscreen')!.shadowRoot,
        CrButtonElement);
    assertTrue(!!testButton);
    testButton.click();
    await flushTasks();
    assertTrue(introDialog.open);

    const getStartedButton =
        strictQuery('cr-button', introDialog, CrButtonElement);
    getStartedButton.click();
    await flushTasks();
    assert(touchscreenTester);
    const canvasDialog = touchscreenTester.getDialog('canvas-dialog');
    assertTrue(canvasDialog.open);

    return touchscreenTester;
  }

  test('InputListPopulated', async () => {
    await initializeInputList();
    assertEquals(
        fakeKeyboards[0]!.name,
        getCardByDeviceType('keyboard')!.devices[0]!.name);
    assertEquals(
        fakeTouchDevices[0]!.name,
        getCardByDeviceType('touchpad')!.devices[0]!.name);
    assertEquals(
        fakeTouchDevices[1]!.name,
        getCardByDeviceType('touchscreen')!.devices[0]!.name);
  });

  test('KeyboardAddAndRemove', async () => {
    const fakeKeyboard: KeyboardInfo = {
      id: 4,
      connectionType: ConnectionType.kUsb,
      name: 'Sample USB keyboard',
      regionCode: 'US',
      physicalLayout: PhysicalLayout.kUnknown,
      mechanicalLayout: MechanicalLayout.kUnknown,
      hasAssistantKey: false,
      numberPadPresent: NumberPadPresence.kUnknown,
      topRowKeys: [
        TopRowKey.kBack,
        TopRowKey.kForward,
        TopRowKey.kRefresh,
        TopRowKey.kFullscreen,
        TopRowKey.kOverview,
        TopRowKey.kScreenBrightnessDown,
        TopRowKey.kScreenBrightnessUp,
        TopRowKey.kVolumeMute,
        TopRowKey.kVolumeDown,
        TopRowKey.kVolumeUp,
      ],
      topRightKey: TopRightKey.kUnknown,
    };
    await initializeInputList();
    const keyboardCard = getCardByDeviceType('keyboard');
    assert(keyboardCard);
    provider.addFakeConnectedKeyboard(fakeKeyboard);
    await flushTasks();
    assertEquals(2, keyboardCard.devices.length);
    assertEquals(fakeKeyboards[0]!.name, keyboardCard.devices[0]!.name);
    assertEquals(fakeKeyboard.name, keyboardCard.devices[1]!.name);

    provider.removeFakeConnectedKeyboardById(fakeKeyboard.id);
    await flushTasks();
    assertEquals(1, keyboardCard.devices.length);
    assertEquals(fakeKeyboards[0]!.name, keyboardCard.devices[0]!.name);
  });

  test('KeyboardTesterShow', async () => {
    const keyboardInfo: KeyboardInfo|undefined = fakeKeyboards[0];
    assert(keyboardInfo);
    await initializeInputList([keyboardInfo], []);
    const testButton = strictQuery(
        'cr-button', getCardByDeviceType('keyboard')!.shadowRoot,
        CrButtonElement);
    assert(testButton);
    testButton.click();
    await flushTasks();
    assert(inputListElement);
    const keyboardTester = strictQuery(
        'keyboard-tester', inputListElement.shadowRoot, KeyboardTesterElement);
    assert(keyboardTester);
    assertTrue(keyboardTester.isOpen());
  });

  test('KeyboardTesterShowDirectlyWithOneKeyboard', async () => {
    const params = new URLSearchParams(window.location.search);
    params.set('showDefaultKeyboardTester', '');
    window.history.replaceState(
        null, '', `${window.location.pathname}?${params.toString()}`);

    const keyboardInfo: KeyboardInfo|undefined = fakeKeyboards[0];
    assert(keyboardInfo);
    await initializeInputList([keyboardInfo], []);
    await flushTasks();
    assert(inputListElement);
    const keyboardTester = strictQuery(
        'keyboard-tester', inputListElement.shadowRoot, KeyboardTesterElement);
    assert(keyboardTester);
    assertTrue(keyboardTester.isOpen());
  });

  test('KeyboardTesterShowDirectlyWithNoKeyboard', async () => {
    const params = new URLSearchParams(window.location.search);
    params.set('showDefaultKeyboardTester', '');
    window.history.replaceState(
        null, '', `${window.location.pathname}?${params.toString()}`);

    await initializeInputList([], []);
    assert(inputListElement);
    await flushTasks();
    const keyboardTester = strictQuery(
        'keyboard-tester', inputListElement.shadowRoot, KeyboardTesterElement);
    assert(keyboardTester);
    assertFalse(keyboardTester.isOpen());
  });

  test('KeyboardTesterCloseOnLidClosed', async () => {
    const keyboardInfo: KeyboardInfo|undefined = fakeKeyboards[0];
    assert(keyboardInfo);
    await initializeInputList([keyboardInfo], []);
    const testButton = strictQuery(
        'cr-button', getCardByDeviceType('keyboard')!.shadowRoot,
        CrButtonElement);
    assertTrue(!!testButton);
    testButton.click();
    await flushTasks();
    assert(inputListElement);
    const keyboardTester = strictQuery(
        'keyboard-tester', inputListElement.shadowRoot, KeyboardTesterElement);
    assert(keyboardTester);
    assertTrue(keyboardTester.isOpen());

    const showToastEvent = eventToPromise('show-toast', inputListElement);
    provider.setLidStateClosed();
    await flushTasks();
    assertFalse(keyboardTester.isOpen());

    const e = await showToastEvent;
    assertEquals(
        e.detail.message,
        loadTimeData.getString('inputKeyboardTesterClosedToastLidClosed'));
  });

  test('KeyboardTesterCloseOnTabletMode', async () => {
    const keyboardInfo: KeyboardInfo|undefined = fakeKeyboards[0];
    assert(keyboardInfo);
    await initializeInputList([keyboardInfo], []);
    const testButton = strictQuery(
        'cr-button', getCardByDeviceType('keyboard')!.shadowRoot,
        CrButtonElement);
    assert(testButton);
    testButton.click();
    await flushTasks();
    assert(inputListElement);
    const keyboardTester = strictQuery(
        'keyboard-tester', inputListElement.shadowRoot, KeyboardTesterElement);
    assert(keyboardTester);
    assertTrue(keyboardTester.isOpen());

    const showToastEvent = eventToPromise('show-toast', inputListElement);
    provider.startTabletMode();
    await flushTasks();
    assertFalse(keyboardTester.isOpen());

    const e = await showToastEvent;
    assertEquals(
        e.detail.message,
        loadTimeData.getString('inputKeyboardTesterClosedToastTabletMode'));
  });

  test('ShowToastIfKeyboardDisconnectedDuringTest', async () => {
    const keyboardInfo: KeyboardInfo|undefined = fakeKeyboards[0];
    assert(keyboardInfo);
    await initializeInputList([keyboardInfo], []);
    const testButton = strictQuery(
        'cr-button', getCardByDeviceType('keyboard')!.shadowRoot,
        CrButtonElement);
    assert(testButton);
    testButton.click();
    await flushTasks();
    assert(inputListElement);
    const keyboardTester = strictQuery(
        'keyboard-tester', inputListElement.shadowRoot, KeyboardTesterElement);
    assert(keyboardTester);
    assertTrue(keyboardTester.isOpen());
    const showToastEvent = eventToPromise('show-toast', inputListElement);
    // Remove keyboard while tester is open.
    provider.removeFakeConnectedKeyboardById(keyboardInfo.id);
    await flushTasks();
    // Verify that the custom event was dispatched
    const e = await showToastEvent;
    assertEquals(
        e.detail.message, loadTimeData.getString('deviceDisconnected'));

    // Verify that tester is closed.
    assertFalse(keyboardTester.isOpen());

    // Verify that key events are no longer blocked by keyboard-tester.
    let keyEventReceived = false;
    inputListElement.addEventListener('keydown', () => keyEventReceived = true);
    const keyDownEvent = eventToPromise('keydown', inputListElement);
    keyboardTester.dispatchEvent(new KeyboardEvent(
        'keydown', {bubbles: true, key: 'A', composed: true}));
    await keyDownEvent;
    assertTrue(keyEventReceived);
  });

  test('TouchpadAddAndRemove', async () => {
    const fakeTouchpad: TouchDeviceInfo = {
      id: 4,
      connectionType: ConnectionType.kUsb,
      type: TouchDeviceType.kPointer,
      name: 'Sample USB touchpad',
      testable: true,
    };
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[0];
    assert(touchDevice);
    await initializeInputList();
    const touchpadCard = getCardByDeviceType('touchpad');
    assert(touchpadCard);
    provider.addFakeConnectedTouchDevice(fakeTouchpad);
    await flushTasks();
    assertEquals(2, touchpadCard.devices.length);
    assertEquals(touchDevice.name, touchpadCard.devices[0]!.name);
    assertEquals(fakeTouchpad.name, touchpadCard.devices[1]!.name);

    provider.removeFakeConnectedTouchDeviceById(fakeTouchpad.id);
    await flushTasks();
    assertEquals(1, touchpadCard.devices.length);
    assertEquals(touchDevice.name, touchpadCard.devices[0]!.name);
  });

  test('TouchpadTesterShowAndClose', async () => {
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[0];
    assert(touchDevice);
    await initializeInputList([], [touchDevice]);
    assert(inputListElement);
    const touchpadTester =
        inputListElement!.shadowRoot!.querySelector('touchpad-tester');
    assert(touchpadTester);
    assertFalse(touchpadTester.isOpen());

    const testButton = strictQuery(
        'cr-button', getCardByDeviceType('touchpad')!.shadowRoot,
        CrButtonElement);
    assert(testButton);
    testButton.click();
    await flushTasks();

    assertTrue(touchpadTester.isOpen());
    assertDeepEquals(touchDevice, touchpadTester.touchpad);
  });

  test('TouchscreenAddAndRemove', async () => {
    const fakeTouchscreen: TouchDeviceInfo = {
      id: 4,
      connectionType: ConnectionType.kUsb,
      type: TouchDeviceType.kDirect,
      name: 'Sample USB touchscreen',
      testable: true,
    };
    await initializeInputList();
    const touchscreenCard = getCardByDeviceType('touchscreen');
    assert(touchscreenCard);
    provider.addFakeConnectedTouchDevice(fakeTouchscreen);
    await flushTasks();
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[1];
    assert(touchDevice);
    assertEquals(3, touchscreenCard.devices.length);
    assertEquals(touchDevice.name, touchscreenCard.devices[0]!.name);
    assertEquals(fakeTouchscreen.name, touchscreenCard.devices[2]!.name);

    provider.removeFakeConnectedTouchDeviceById(fakeTouchscreen.id);
    await flushTasks();
    assertEquals(2, touchscreenCard.devices.length);
    assertEquals(touchDevice.name, touchscreenCard.devices[0]!.name);
  });

  test('TouchscreenTesterShowAndClose', async () => {
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[1];
    assert(touchDevice);
    await initializeInputList([], [touchDevice]);
    provider.setStartTesterWithClamshellMode();

    const resolver = new PromiseResolver();
    let requestFullscreenCalled = 0;
    assert(inputListElement);
    const touchscreenTester =
        inputListElement!.shadowRoot!.querySelector('touchscreen-tester');
    assert(touchscreenTester);
    const introDialog =
        touchscreenTester.getDialog('intro-dialog') as CrDialogElement;

    // Mock requestFullscreen function since this API can only be initiated by a
    // user gesture.
    introDialog.requestFullscreen = (): any => {
      requestFullscreenCalled++;
      resolver.resolve(undefined);
    };

    let eventTrackerRemoveAllCalled = 0;
    touchscreenTester.getEventTracker().removeAll = () => {
      eventTrackerRemoveAllCalled++;
      resolver.resolve(undefined);
    };

    // A11y touch passthrough state is false by default.
    assertFalse(provider.getA11yTouchPassthroughState());

    const testButton = strictQuery(
        'cr-button', getCardByDeviceType('touchscreen')!.shadowRoot,
        CrButtonElement);
    assert(testButton);
    testButton.click();
    await flushTasks();
    assertEquals(1, requestFullscreenCalled);
    assertTrue(introDialog.open);
    assertEquals(
        /*expectedMoveAppToTestingScreenCalled=*/ 1,
        provider.getMoveAppToTestingScreenCalled());

    // Click get stated button to open canvas dialog.
    const getStartedButton =
        introDialog.querySelector('cr-button') as CrButtonElement;
    getStartedButton.click();
    await flushTasks();
    assertFalse(introDialog.open);
    assertTrue(provider.getA11yTouchPassthroughState());

    const canvasDialog =
        touchscreenTester.getDialog('canvas-dialog') as CrDialogElement;
    assertTrue(canvasDialog.open);

    const fullscreenChangeEvent = eventToPromise('fullscreenchange', document);
    document.dispatchEvent(new Event('fullscreenchange'));
    await fullscreenChangeEvent;

    assertFalse(canvasDialog.open);
    assertEquals(
        /*expectedMoveAppBackToPreviousScreenCalled=*/ 1,
        provider.getMoveAppBackToPreviousScreenCalled());
    assertEquals(
        /*expectedEventTrackerRemoveAllCalled=*/ 1,
        eventTrackerRemoveAllCalled);
    assertFalse(provider.getA11yTouchPassthroughState());
  });

  test('TouchscreenTesterShowAndCloseInTabletMode', async () => {
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[1];
    assert(touchDevice);
    await openTouchscreenTester(
        /*touchscreensToInitialize=*/[touchDevice]);

    const resolver = new PromiseResolver();
    let exitFullscreenCalled = 0;
    // Mock exitFullscreen call.
    document.exitFullscreen = (): any => {
      exitFullscreenCalled++;
      resolver.resolve(undefined);
    };

    provider.startTabletMode();
    await flushTasks();

    const keyEvent = eventToPromise('keydown', window);
    window.dispatchEvent(new KeyboardEvent('keydown', {key: 'AudioVolumeUp'}));
    await keyEvent;
    assertEquals(1, exitFullscreenCalled);
  });

  test('StartTouchscreenTesterWithClamshellMode', async () => {
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[1];
    assert(touchDevice);
    const touchscreenTester = await openTouchscreenTester(
        /*touchscreensToInitialize=*/[touchDevice]);
    assert(touchscreenTester);
    assertFalse(touchscreenTester.getIsTabletMode());

    provider.startTabletMode();
    await flushTasks();

    assertTrue(touchscreenTester.getIsTabletMode());
    assertTrue(
        (touchscreenTester.getDialog('canvas-dialog') as CrDialogElement).open);
  });

  test('StartTouchscreenTesterWithTabletMode', async () => {
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[1];
    assert(touchDevice);
    const touchscreenTester = await openTouchscreenTester(
        /*touchscreensToInitialize=*/[touchDevice],
        /*isClamshellMode=*/ false);
    assert(touchscreenTester);
    assertTrue(touchscreenTester.getIsTabletMode());

    provider.endTabletMode();
    await flushTasks();

    assertFalse(touchscreenTester.getIsTabletMode());
    assertTrue(
        (touchscreenTester.getDialog('canvas-dialog') as CrDialogElement).open);
  });

  test('OnInternalDisplayPowerStateChanged', async () => {
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[1];
    assert(touchDevice);
    await initializeInputList([], [touchDevice]);

    assertTrue(
        (getCardByDeviceType('touchscreen')!.devices[0] as TouchDeviceInfo)
            .testable);

    provider.setInternalDisplayPowerOff();
    await flushTasks();

    assertFalse(
        (getCardByDeviceType('touchscreen')!.devices[0] as TouchDeviceInfo)
            .testable);

    provider.setInternalDisplayPowerOn();
    await flushTasks();

    assertTrue(
        (getCardByDeviceType('touchscreen')!.devices[0] as TouchDeviceInfo)
            .testable);
  });

  test('InternalDisplayPowerOffWhileTouchscreenTesterIsRunning', async () => {
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[1];
    assert(touchDevice);
    const touchscreenTester = await openTouchscreenTester(
        /*touchscreensToInitialize=*/[touchDevice]);
    assert(touchscreenTester);
    // Internal display power off.
    provider.setInternalDisplayPowerOff();
    await flushTasks();

    // Tester is expecetd to exit.
    assertFalse(
        (touchscreenTester.getDialog('canvas-dialog') as CrDialogElement).open);
  });

  test('TouchscreenDisconnectedWhileTesterIsRunning', async () => {
    const touchDevice: TouchDeviceInfo|undefined = fakeTouchDevices[1];
    assert(touchDevice);
    const touchscreenTester = await openTouchscreenTester(
        /*touchscreensToInitialize=*/[touchDevice]);

    // Remove touchscreen.
    provider.removeFakeConnectedTouchDeviceById(touchDevice.id);
    await flushTasks();

    // Tester is expecetd to exit.
    assertFalse(
        (touchscreenTester.getDialog('canvas-dialog') as CrDialogElement).open);
  });

  test('EmptySectionsHidden', async () => {
    await initializeInputList([], []);
    assertFalse(isVisible(getCardByDeviceType('keyboard')));
    assertFalse(isVisible(getCardByDeviceType('touchpad')));
    assertFalse(isVisible(getCardByDeviceType('touchscreen')));

    provider.addFakeConnectedTouchDevice(
        (fakeTouchDevices[1] as TouchDeviceInfo));
    await flushTasks();
    assertTrue(!!getCardByDeviceType('touchscreen'));
    assertFalse(isVisible(getCardByDeviceType('keyboard')));
    assertFalse(isVisible(getCardByDeviceType('touchpad')));
    assertTrue(isVisible(getCardByDeviceType('touchscreen')));

    provider.removeFakeConnectedTouchDeviceById(fakeTouchDevices[1]!.id);
    provider.addFakeConnectedTouchDevice(
        (fakeTouchDevices[0] as TouchDeviceInfo));
    await flushTasks();
    assertFalse(isVisible(getCardByDeviceType('keyboard')));
    assertTrue(isVisible(getCardByDeviceType('touchpad')));
    assertFalse(isVisible(getCardByDeviceType('touchscreen')));

    provider.removeFakeConnectedTouchDeviceById(fakeTouchDevices[0]!.id);
    provider.addFakeConnectedKeyboard((fakeKeyboards[0] as KeyboardInfo));
    await flushTasks();
    assertTrue(isVisible(getCardByDeviceType('keyboard')));
    assertFalse(isVisible(getCardByDeviceType('touchpad')));
    assertFalse(isVisible(getCardByDeviceType('touchscreen')));

    provider.removeFakeConnectedKeyboardById(fakeKeyboards[0]!.id);
    await flushTasks();
    assertFalse(isVisible(getCardByDeviceType('keyboard')));
    assertFalse(isVisible(getCardByDeviceType('touchpad')));
    assertFalse(isVisible(getCardByDeviceType('touchscreen')));
  });

  test('RecordNavigationCalled', async () => {
    await initializeInputList();
    assert(inputListElement);
    inputListElement.onNavigationPageChanged({isActive: false});
    await flushTasks();

    assertEquals(0, diagnosticsBrowserProxy.getCallCount('recordNavigation'));

    diagnosticsBrowserProxy.setPreviousView(NavigationView.SYSTEM);
    inputListElement.onNavigationPageChanged({isActive: true});

    await flushTasks();
    assertEquals(1, diagnosticsBrowserProxy.getCallCount('recordNavigation'));
    assertArrayEquals(
        [NavigationView.SYSTEM, NavigationView.INPUT],
        (diagnosticsBrowserProxy.getArgs('recordNavigation')[0]));
  });
});