chromium/chrome/test/data/webui/chromeos/settings/os_people_page/fingerprint_list_subpage_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.

import 'chrome://os-settings/lazy_load.js';

import {FingerprintBrowserProxyImpl, FingerprintResultType, FingerprintSetupStep, SettingsFingerprintListSubpageElement, SettingsSetupFingerprintDialogElement} from 'chrome://os-settings/lazy_load.js';
import {CrDialogElement, Router, routes} from 'chrome://os-settings/os_settings.js';
import {loadTimeData} from 'chrome://resources/js/load_time_data.js';
import {getDeepActiveElement} from 'chrome://resources/js/util.js';
import {DomRepeatEvent, flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {flushTasks, waitAfterNextRender} from 'chrome://webui-test/polymer_test_util.js';
import {isVisible} from 'chrome://webui-test/test_util.js';

import {FakeQuickUnlockPrivate} from '../fake_quick_unlock_private.js';

import {TestFingerprintBrowserProxy} from './test_fingerprint_browser_proxy.js';

suite('<settings-fingerprint-list-subpage>', () => {
  let fingerprintList: SettingsFingerprintListSubpageElement;
  let dialog: SettingsSetupFingerprintDialogElement;
  let addAnotherButton: HTMLButtonElement;
  let browserProxy: TestFingerprintBrowserProxy;

  function createFakeEvent(index: number, label?: string) {
    return {model: {index: index, item: label || ''}} as DomRepeatEvent<string>;
  }

  function openDialog() {
    const actionButton =
        fingerprintList.shadowRoot!.querySelector<HTMLButtonElement>(
            '.action-button');
    assertTrue(!!actionButton);
    actionButton.click();
    flush();
    const dialogElement = fingerprintList.shadowRoot!.querySelector(
        'settings-setup-fingerprint-dialog');
    assertTrue(!!dialogElement);
    dialog = dialogElement;
    const button = dialog.shadowRoot!.querySelector<HTMLButtonElement>(
        '#addAnotherButton');
    assertTrue(!!button);
    addAnotherButton = button;
  }

  setup(async () => {
    browserProxy = new TestFingerprintBrowserProxy();
    FingerprintBrowserProxyImpl.setInstanceForTesting(browserProxy);

    fingerprintList =
        document.createElement('settings-fingerprint-list-subpage');
    document.body.appendChild(fingerprintList);
    flush();
    await browserProxy.whenCalled('getFingerprintsList');
    assertEquals(0, fingerprintList.get('fingerprints_').length);
    browserProxy.resetResolver('getFingerprintsList');
  });

  test('EnrollingFingerprintLottieAnimation', async () => {
    loadTimeData.overrideValues({fingerprintUnlockEnabled: true});
    openDialog();
    await browserProxy.whenCalled('startEnroll');
    const dialogButton =
        dialog.shadowRoot!.querySelector<CrDialogElement>('#dialog');
    assertTrue(!!dialogButton);
    assertTrue(dialogButton.open);
    assertEquals(FingerprintSetupStep.LOCATE_SCANNER, dialog.get('step_'));
    const element =
        dialog.shadowRoot!.querySelector<HTMLElement>('#scannerLocationLottie');
    assertTrue(!!element);
    assertFalse(element.hidden);
  });

  // Verify running through the enroll session workflow
  // (settings-setup-fingerprint-dialog) works as expected.
  test('EnrollingFingerprint', async () => {
    loadTimeData.overrideValues({fingerprintUnlockEnabled: true});
    openDialog();
    await browserProxy.whenCalled('startEnroll');
    const dialogButton =
        dialog.shadowRoot!.querySelector<CrDialogElement>('#dialog');
    assertTrue(!!dialogButton);
    assertTrue(dialogButton.open);
    assertEquals(0, dialog.get('percentComplete_'));
    assertEquals(FingerprintSetupStep.LOCATE_SCANNER, dialog.get('step_'));
    const element =
        dialog.shadowRoot!.querySelector<HTMLElement>('#scannerLocationLottie');
    assertTrue(!!element);
    assertFalse(element.hidden);
    const arcElement = dialog.shadowRoot!.querySelector<HTMLElement>('#arc');
    assertTrue(!!arcElement);
    assertTrue(arcElement.hidden);
    // Message should be shown for LOCATE_SCANNER step.
    const message = dialog.shadowRoot!.querySelector('#messageDiv');
    assertTrue(!!message);
    assertEquals('visible', window.getComputedStyle(message).visibility);

    // First tap on the sensor to start fingerprint enrollment.
    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, false, 20 /* percent */);
    assertEquals(20, dialog.get('percentComplete_'));
    assertEquals(FingerprintSetupStep.MOVE_FINGER, dialog.get('step_'));
    assertTrue(element.hidden);
    assertFalse(arcElement.hidden);

    // Verify that by sending a scan problem, the div that contains the
    // problem message should be visible.
    browserProxy.scanReceived(
        FingerprintResultType.TOO_FAST, false, 20 /* percent */);
    assertEquals(20, dialog.get('percentComplete_'));
    assertEquals('visible', window.getComputedStyle(message).visibility);
    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, false, 50 /* percent */);
    assertEquals('hidden', window.getComputedStyle(message).visibility);
    assertEquals(50, dialog.get('percentComplete_'));
    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, false, 70 /* percent */);
    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, true, 100 /* percent */);
    assertEquals(FingerprintSetupStep.READY, dialog.get('step_'));
    // Message should be shown for READY step.
    assertEquals('visible', window.getComputedStyle(message).visibility);

    // Verify that by tapping the continue button we should exit the dialog
    // and the fingerprint list should have one fingerprint registered.
    const closeButton =
        dialog.shadowRoot!.querySelector<HTMLButtonElement>('#closeButton');
    assertTrue(!!closeButton);
    closeButton.click();
    await flushTasks();
    await browserProxy.whenCalled('getFingerprintsList');
    assertEquals(1, fingerprintList.get('fingerprints_').length);
  });

  // Verify enrolling a fingerprint, then enrolling another without closing the
  // dialog works as intended.
  test('EnrollingAnotherFingerprint', async () => {
    loadTimeData.overrideValues({fingerprintUnlockEnabled: true});
    openDialog();
    await browserProxy.whenCalled('startEnroll');
    browserProxy.resetResolver('startEnroll');

    const dialogButton =
        dialog.shadowRoot!.querySelector<CrDialogElement>('#dialog');
    assertTrue(!!dialogButton);
    assertTrue(dialogButton.open);
    assertEquals(0, dialog.get('percentComplete_'));
    assertFalse(isVisible(addAnotherButton));
    assertEquals(FingerprintSetupStep.LOCATE_SCANNER, dialog.get('step_'));

    // First tap on the sensor to start fingerprint enrollment.
    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, false, 20 /* percent */);
    assertEquals(FingerprintSetupStep.MOVE_FINGER, dialog.get('step_'));

    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, true, 100 /* percent */);
    assertEquals(FingerprintSetupStep.READY, dialog.get('step_'));

    assertTrue(dialogButton.open);
    assertTrue(isVisible(addAnotherButton));
    addAnotherButton.click();

    // Once the first fingerprint is enrolled, verify that enrolling the
    // second fingerprint without closing the dialog works as expected.
    await Promise.all([
      browserProxy.whenCalled('startEnroll'),
      browserProxy.whenCalled('getFingerprintsList'),
    ]);
    browserProxy.resetResolver('getFingerprintsList');

    assertTrue(dialogButton.open);
    assertFalse(isVisible(addAnotherButton));
    assertEquals(FingerprintSetupStep.MOVE_FINGER, dialog.get('step_'));
    const element =
        dialog.shadowRoot!.querySelector<HTMLElement>('#scannerLocationLottie');
    assertTrue(!!element);
    assertTrue(element.hidden);
    const arcElement = dialog.shadowRoot!.querySelector<HTMLElement>('#arc');
    assertTrue(!!arcElement);
    assertFalse(arcElement.hidden);

    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, true, 100 /* percent */);

    // Verify that by tapping the continue button we should exit the
    // dialog and the fingerprint list should have two fingerprints
    // registered.
    const closeButton =
        dialog.shadowRoot!.querySelector<HTMLButtonElement>('#closeButton');
    assertTrue(!!closeButton);
    closeButton.click();
    await browserProxy.whenCalled('getFingerprintsList');
    assertEquals(2, fingerprintList.get('fingerprints_').length);
  });

  // Verify after third fingerprint is enrolled, add another button in the
  // setup dialog is hidden.
  test('EnrollingThirdFingerprint', async () => {
    browserProxy.setFingerprints(['1', '2']);
    fingerprintList['updateFingerprintsList_']();

    openDialog();
    await browserProxy.whenCalled('startEnroll');
    browserProxy.resetResolver('startEnroll');

    const dialogButton =
        dialog.shadowRoot!.querySelector<CrDialogElement>('#dialog');
    assertTrue(!!dialogButton);
    assertTrue(dialogButton.open);
    assertEquals(0, dialog.get('percentComplete_'));
    assertFalse(isVisible(addAnotherButton));
    assertEquals(FingerprintSetupStep.LOCATE_SCANNER, dialog.get('step_'));

    // First tap on the sensor to start fingerprint enrollment.
    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, false, 20 /* percent */);
    assertEquals(FingerprintSetupStep.MOVE_FINGER, dialog.get('step_'));

    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, true, 100 /* percent */);
    assertEquals(FingerprintSetupStep.READY, dialog.get('step_'));
    await browserProxy.whenCalled('getFingerprintsList');
    browserProxy.resetResolver('getFingerprintsList');

    // Add another is hidden after third fingerprint is enrolled.
    assertTrue(dialogButton.open);
    assertFalse(isVisible(addAnotherButton));
    assertEquals(3, fingerprintList.get('fingerprints_').length);
  });

  test('CancelEnrollingFingerprint', async () => {
    openDialog();
    await browserProxy.whenCalled('startEnroll');
    const dialogButton =
        dialog.shadowRoot!.querySelector<CrDialogElement>('#dialog');
    assertTrue(!!dialogButton);
    assertTrue(dialogButton.open);
    assertEquals(0, dialog.get('percentComplete_'));
    assertEquals(FingerprintSetupStep.LOCATE_SCANNER, dialog.get('step_'));
    // First tap on the sensor to start fingerprint enrollment.
    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, false, 20 /* percent */);
    assertEquals(FingerprintSetupStep.MOVE_FINGER, dialog.get('step_'));

    browserProxy.scanReceived(
        FingerprintResultType.SUCCESS, false, 30 /* percent */);
    assertEquals(30, dialog.get('percentComplete_'));
    assertEquals(FingerprintSetupStep.MOVE_FINGER, dialog.get('step_'));

    // Verify that by tapping the exit button we should exit the dialog
    // and the fingerprint list should have zero fingerprints registered.
    const closeButton =
        dialog.shadowRoot!.querySelector<HTMLButtonElement>('#closeButton');
    assertTrue(!!closeButton);
    closeButton.click();
    await browserProxy.whenCalled('cancelCurrentEnroll');
    assertEquals(0, fingerprintList.get('fingerprints_').length);
  });

  test('RemoveFingerprint', async () => {
    const quickUnlockPrivateApi = new FakeQuickUnlockPrivate();
    fingerprintList.set('authToken', quickUnlockPrivateApi.getFakeToken());
    browserProxy.setFingerprints(['Label 1', 'Label 2']);
    fingerprintList['updateFingerprintsList_']();
    await browserProxy.whenCalled('getFingerprintsList');
    browserProxy.resetResolver('getFingerprintsList');
    assertEquals(2, fingerprintList.get('fingerprints_').length);
    fingerprintList['onFingerprintDeleteTapped_'](createFakeEvent(0));

    await Promise.all([
      browserProxy.whenCalled('removeEnrollment'),
      browserProxy.whenCalled('getFingerprintsList'),
    ]);
    assertEquals(1, fingerprintList.get('fingerprints_').length);
  });

  test('Deep link to add fingerprint', async () => {
    const quickUnlockPrivateApi = new FakeQuickUnlockPrivate();
    fingerprintList.set('authToken', quickUnlockPrivateApi.getFakeToken());
    // This is equivalent to the settings id.
    const settingId = '1111';
    browserProxy.setFingerprints(['Label 1', 'Label 2']);
    fingerprintList['updateFingerprintsList_']();
    await browserProxy.whenCalled('getFingerprintsList');

    const params = new URLSearchParams();
    params.append('settingId', settingId);
    Router.getInstance().navigateTo(routes.FINGERPRINT, params);

    flush();

    const deepLinkElement =
        fingerprintList.shadowRoot!.querySelector<HTMLElement>(
            '#addFingerprint');
    assertTrue(!!deepLinkElement);
    await waitAfterNextRender(deepLinkElement);
    assertEquals(
        deepLinkElement, getDeepActiveElement(),
        'Add button should be focused for settingId=' + settingId);
  });

  test('Deep link to remove fingerprint', async () => {
    const settingId = '1112';

    browserProxy.setFingerprints(['Label 1', 'Label 2']);
    const quickUnlockPrivateApi = new FakeQuickUnlockPrivate();
    fingerprintList.set('authToken', quickUnlockPrivateApi.getFakeToken());
    fingerprintList['updateFingerprintsList_']();
    await browserProxy.whenCalled('getFingerprintsList');

    const params = new URLSearchParams();
    params.append('settingId', settingId);
    Router.getInstance().navigateTo(routes.FINGERPRINT, params);

    flush();

    const deepLinkElement =
        fingerprintList.root!.querySelectorAll('cr-icon-button')[0];
    assertTrue(!!deepLinkElement);
    await waitAfterNextRender(deepLinkElement);
    assertEquals(
        deepLinkElement, getDeepActiveElement(),
        'Trash can button should be focused for settingId=' + settingId);
  });

  test('ChangeFingerprintLabel', async () => {
    browserProxy.setFingerprints(['Label 1']);
    fingerprintList['updateFingerprintsList_']();

    await browserProxy.whenCalled('getFingerprintsList');
    assertEquals(1, fingerprintList.get('fingerprints_').length);
    assertEquals('Label 1', fingerprintList.get('fingerprints_')[0]);

    // Verify that by sending a fingerprint input change event, the new
    // label gets changed as expected.
    fingerprintList['onFingerprintLabelChanged_'](
        createFakeEvent(0, 'New Label 1'));

    await Promise.all([
      browserProxy.whenCalled('changeEnrollmentLabel'),
      browserProxy.whenCalled('getFingerprintsList'),
    ]);
    assertEquals('New Label 1', fingerprintList.get('fingerprints_')[0]);
  });

  test('AddingNewFingerprint', async () => {
    browserProxy.setFingerprints(['1', '2', '3']);
    fingerprintList['updateFingerprintsList_']();
    const quickUnlockPrivateApi = new FakeQuickUnlockPrivate();
    fingerprintList.set('authToken', quickUnlockPrivateApi.getFakeToken());
    // Verify that new fingerprints cannot be added when there are already three
    // registered fingerprints.
    await browserProxy.whenCalled('getFingerprintsList');
    browserProxy.resetResolver('getFingerprintsList');
    assertEquals(3, fingerprintList.get('fingerprints_').length);
    const actionButton =
        fingerprintList.shadowRoot!.querySelector<HTMLButtonElement>(
            '.action-button');
    assertTrue(!!actionButton);
    assertTrue(actionButton.disabled);
    fingerprintList['onFingerprintDeleteTapped_'](createFakeEvent(0));

    await Promise.all([
      browserProxy.whenCalled('removeEnrollment'),
      browserProxy.whenCalled('getFingerprintsList'),
    ]);
    assertEquals(2, fingerprintList.get('fingerprints_').length);
    assertFalse(actionButton.disabled);
  });
});