chromium/ui/file_manager/file_manager/widgets/xf_password_dialog_unittest.ts

// Copyright 2023 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://resources/ash/common/cr_elements/cr_button/cr_button.js';
import 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import './xf_password_dialog.js';

import {assert} from 'chrome://resources/ash/common/assert.js';
import type {CrButtonElement} from 'chrome://resources/ash/common/cr_elements/cr_button/cr_button.js';
import type {CrDialogElement} from 'chrome://resources/ash/common/cr_elements/cr_dialog/cr_dialog.js';
import type {CrInputElement} from 'chrome://resources/ash/common/cr_elements/cr_input/cr_input.js';
import {getTrustedHTML} from 'chrome://resources/js/static_types.js';
import {assertEquals, assertFalse, assertNotReached} from 'chrome://webui-test/chromeos/chai_assert.js';

import {waitUntil} from '../common/js/test_error_reporting.js';

import type {XfPasswordDialog} from './xf_password_dialog.js';
import {USER_CANCELLED} from './xf_password_dialog.js';

let passwordDialog: XfPasswordDialog;
let dialog: CrDialogElement;
let name: HTMLElement;
let input: CrInputElement;
let cancel: CrButtonElement;
let unlock: CrButtonElement;

/**
 * Adds a XfPasswordDialog element to the page.
 */
export function setUp() {
  document.body.innerHTML = getTrustedHTML
  `<xf-password-dialog hidden>
    </xf-password-dialog>`;

  // Get the <xf-password-dialog> element.
  passwordDialog = document.querySelector('xf-password-dialog')!;

  // Get its sub-elements.
  dialog = passwordDialog.shadowRoot!.querySelector('#password-dialog') as
      CrDialogElement;
  name = passwordDialog.shadowRoot!.querySelector('#name')!;
  input = passwordDialog.shadowRoot!.querySelector('#input')!;
  cancel = passwordDialog.shadowRoot!.querySelector('#cancel')!;
  unlock = passwordDialog.shadowRoot!.querySelector('#unlock')!;
}

/**
 * Tests that the password dialog is modal.
 */
export async function testPasswordDialogIsModal(done: () => void) {
  // Check that the dialog is closed.
  assertFalse(dialog.open);

  // Open the password dialog.
  passwordDialog.askForPassword('encrypted.zip');

  // Wait until the dialog is open.
  await waitUntil(() => dialog.open);

  // Check that the name of the encrypted zip displays correctly on the dialog.
  assertEquals('encrypted.zip', name.innerText);

  // Enter password.
  input.value = 'password';

  // Keyboard events should not propagate out to the <xf-password-dialog>
  // element. The internal <cr-dialog> element should handle keyboard events
  // and should prevent them from bubbling up to the <xf-password-dialog>
  // element (modal operation).
  let isModal = true;
  passwordDialog.addEventListener('keydown', event => {
    console.error('<xf-password-dialog> keydown event', event);
    isModal = false;
  });

  // Add a <cr-dialog> listener to confirm it saw the test keyboard event.
  let keydownSeen = false;
  dialog.addEventListener('keydown', () => {
    keydownSeen = true;
  });

  // Create a <ctrl>-A keyboard event. The event has 'composed' true, so it
  // can cross shadow DOM bondaries and bubble up into the DOM document.
  const keyEvent = new KeyboardEvent('keydown', {
    bubbles: true,
    cancelable: true,
    composed: true,
    ctrlKey: true,
    key: 'A',
  });

  // Dispatch the keyboard event to the <cr-input> inner <input> element.
  const inputElement =
      input.shadowRoot!.querySelector('input') as HTMLInputElement;
  assertEquals(inputElement.value, 'password');
  assert(inputElement.dispatchEvent(keyEvent));

  // Check: the <xf-password-dialog> should be modal.
  await waitUntil(() => keydownSeen);
  assert(isModal, 'FAILED: <xf-password-dialog> should be modal');

  done();
}

/**
 * Tests cancel functionality of password dialog for single encrypted archive.
 * The askForPassword method should return a promise that is rejected with
 * USER_CANCELLED.
 */
export async function testSingleArchiveCancelPasswordPrompt(done: () => void) {
  // Check that the dialog is closed.
  assertFalse(dialog.open);

  // Open password prompt.
  const passwordPromise = passwordDialog.askForPassword('encrypted.zip');

  // Wait until the dialog is open.
  await waitUntil(() => dialog.open);

  // Check that the name of the encrypted zip displays correctly on the dialog.
  assertEquals('encrypted.zip', name.innerText);

  // Enter password.
  input.value = 'password';

  // Click the cancel button closes the dialog.
  cancel.click();

  // The dialog should be hidden.
  await waitUntil(() => !dialog.open);

  // The passwordPromise should be rejected with
  // USER_CANCELLED.
  try {
    await passwordPromise;
    assertNotReached();
  } catch (e) {
    assertEquals(USER_CANCELLED, e);
  }

  // The password input field should be cleared.
  assertEquals('', input.value);

  // Check that the dialog is closed.
  assertFalse(dialog.open);

  done();
}

/**
 * Tests unlock functionality for single encrypted archive. The askForPassword
 * method should return a promise that resolves with the expected input
 * password.
 */
export async function testUnlockSingleArchive(done: () => void) {
  // Check that the dialog is closed.
  assertFalse(dialog.open);

  // Open password prompt.
  const passwordPromise = passwordDialog.askForPassword('encrypted.zip');

  // Wait until the dialog is open.
  await waitUntil(() => dialog.open);

  // Check that the name of the encrypted zip displays correctly on the dialog.
  assertEquals('encrypted.zip', name.innerText);

  // Enter password.
  input.value = 'password';

  // Click the unlock button.
  unlock.click();

  // Wait until the second password promise is resolved with the password value
  // found in the input field.
  assertEquals('password', await passwordPromise);

  // The password input field should be cleared.
  assertEquals('', input.value);

  // Check that the dialog is closed.
  assertFalse(dialog.open);

  done();
}

/**
 * Tests opening the password dialog with an 'Invalid password' message. This
 * message is displayed when a wrong password was previously entered.
 */
export async function testDialogWithWrongPassword(done: () => void) {
  // Check that the dialog is closed.
  assertFalse(dialog.open);

  // Open password prompt.
  passwordDialog.askForPassword('encrypted.zip', 'wrongpassword');

  // Wait until the dialog is open.
  await waitUntil(() => dialog.open);

  // Check that the name of the encrypted zip displays correctly on the dialog.
  assertEquals('encrypted.zip', name.innerText);

  // Check that the previous erroneous password is prefilled in the dialog.
  assertEquals('wrongpassword', input.value);

  // Check that the previous erroneous password is prefilled in the dialog.
  assertEquals('Invalid password', input.errorMessage);

  done();
}

/**
 * Tests cancel functionality for multiple encrypted archives.
 */
export async function testCancelMultiplePasswordPrompts(done: () => void) {
  // Check that the dialog is closed.
  assertFalse(dialog.open);

  // Simulate password prompt for multiple archives.
  const passwordPromise = passwordDialog.askForPassword('encrypted.zip');
  const passwordPromise2 = passwordDialog.askForPassword('encrypted2.zip');

  // Wait until the dialog is open.
  await waitUntil(() => dialog.open);

  // Check that the name of the encrypted zip displays correctly on the dialog.
  assertEquals('encrypted.zip', name.innerText);

  // Enter password.
  input.value = 'password';

  // Click the cancel button closes the dialog.
  cancel.click();

  // The passwordPromise should be rejected with
  // USER_CANCELLED.
  try {
    await passwordPromise;
    assertNotReached();
  } catch (e) {
    assertEquals(USER_CANCELLED, e);
  }

  // The password input field should be cleared.
  assertEquals('', input.value);

  // Wait for password prompt dialog for second archive.
  await waitUntil(() => name.innerText === 'encrypted2.zip');

  // Enter password.
  input.value = 'password';

  // Click the cancel button closes the dialog.
  cancel.click();

  // The passwordPromise should be rejected with
  // USER_CANCELLED.
  try {
    await passwordPromise2;
    assertNotReached();
  } catch (e) {
    assertEquals(USER_CANCELLED, e);
  }

  // The password input field should be cleared.
  assertEquals('', input.value);

  // Check that the dialog is closed.
  assertFalse(dialog.open);

  done();
}

/**
 * Tests unlock functionality for multiple encrypted archives.
 */
export async function testUnlockMultipleArchives(done: () => void) {
  // Check that the dialog is closed.
  assertFalse(dialog.open);

  // Simulate password prompt for multiple archives.
  const passwordPromise = passwordDialog.askForPassword('encrypted.zip');
  const passwordPromise2 = passwordDialog.askForPassword('encrypted2.zip');

  // Wait until the dialog is open.
  await waitUntil(() => dialog.open);

  // Check that the name of the encrypted zip displays correctly on the dialog.
  assertEquals('encrypted.zip', name.innerText);

  // Enter second password.
  input.value = 'password';

  // Click the unlock button.
  unlock.click();

  // Wait until the second password promise is resolved with the password value
  // found in the input field.
  assertEquals('password', await passwordPromise);

  // Wait until the dialog is open for the second password.
  assertEquals('', input.value);

  // Wait until the dialog is open.
  await waitUntil(() => dialog.open);

  // Check that the name of the encrypted zip displays correctly on the dialog.
  assertEquals('encrypted2.zip', name.innerText);

  // Enter third password.
  input.value = 'password2';

  // Click the unlock button.
  unlock.click();

  // Wait until the second password promise resolves with the password value
  // found in the input field.
  assertEquals('password2', await passwordPromise2);

  // The password input field should be cleared.
  assertEquals('', input.value);

  // Check that the dialog is closed.
  assertFalse(dialog.open);

  done();
}