chromium/chrome/test/data/webui/cr_components/certificate_manager/certificate_manager_test.ts

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

// These test suites test the certificate-manager shared component in the
// context of the Settings privacy page. This simplifies the test setup and
// provides better context for testing.

// clang-format off
import 'chrome://settings/strings.m.js';
import 'chrome://resources/cr_components/certificate_manager/ca_trust_edit_dialog.js';
import 'chrome://resources/cr_components/certificate_manager/certificate_delete_confirmation_dialog.js';
import 'chrome://resources/cr_components/certificate_manager/certificate_password_encryption_dialog.js';
import 'chrome://resources/cr_components/certificate_manager/certificate_list.js';
import 'chrome://resources/cr_components/certificate_manager/certificate_password_decryption_dialog.js';
import 'chrome://resources/cr_components/certificate_manager/certificate_manager.js';
import 'chrome://resources/cr_components/certificate_manager/certificate_subentry.js';

import type {CrInputElement} from 'chrome://resources/cr_elements/cr_input/cr_input.js';
import type {CaTrustEditDialogElement} from 'chrome://resources/cr_components/certificate_manager/ca_trust_edit_dialog.js';
import type {CertificateDeleteConfirmationDialogElement} from 'chrome://resources/cr_components/certificate_manager/certificate_delete_confirmation_dialog.js';
import type {CertificateListElement} from 'chrome://resources/cr_components/certificate_manager/certificate_list.js';
import type {CertificateManagerElement} from 'chrome://resources/cr_components/certificate_manager/certificate_manager.js';
import type { CertificateActionEventDetail} from 'chrome://resources/cr_components/certificate_manager/certificate_manager_types.js';
import {CertificateAction, CertificateActionEvent} from 'chrome://resources/cr_components/certificate_manager/certificate_manager_types.js';
import type {CertificatePasswordDecryptionDialogElement} from 'chrome://resources/cr_components/certificate_manager/certificate_password_decryption_dialog.js';
import type {CertificatePasswordEncryptionDialogElement} from 'chrome://resources/cr_components/certificate_manager/certificate_password_encryption_dialog.js';
import type {CertificateSubentryElement} from 'chrome://resources/cr_components/certificate_manager/certificate_subentry.js';
import type {CaTrustInfo, CertificatesBrowserProxy, CertificatesError, CertificatesOrgGroup, CertificateSubnode} from 'chrome://resources/cr_components/certificate_manager/certificates_browser_proxy.js';
import { CertificatesBrowserProxyImpl, CertificateType} from 'chrome://resources/cr_components/certificate_manager/certificates_browser_proxy.js';
import {webUIListenerCallback} from 'chrome://resources/js/cr.js';
import {keyEventOn} from 'chrome://webui-test/keyboard_mock_interactions.js';
import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {TestBrowserProxy} from 'chrome://webui-test/test_browser_proxy.js';
import {eventToPromise} from 'chrome://webui-test/test_util.js';
// clang-format on

/**
 * A test version of CertificatesBrowserProxy. Provides helper methods
 * for allowing tests to know when a method was called, as well as
 * specifying mock responses.
 */
class TestCertificatesBrowserProxy extends TestBrowserProxy implements
    CertificatesBrowserProxy {
  private caTrustInfo_: CaTrustInfo;
  private certificatesError_: CertificatesError|null = null;

  constructor() {
    super([
      'deleteCertificate',
      'editCaCertificateTrust',
      'exportCertificate',
      'exportPersonalCertificate',
      'exportPersonalCertificatePasswordSelected',
      'getCaCertificateTrust',
      'importCaCertificate',
      'importCaCertificateTrustSelected',
      'importPersonalCertificate',
      'importPersonalCertificatePasswordSelected',
      'importServerCertificate',
      'refreshCertificates',
      'viewCertificate',
    ]);

    this.caTrustInfo_ = {ssl: true, email: true, objSign: true};
  }

  setCaCertificateTrust(caTrustInfo: CaTrustInfo) {
    this.caTrustInfo_ = caTrustInfo;
  }

  getCaCertificateTrust(id: string) {
    this.methodCalled('getCaCertificateTrust', id);
    return Promise.resolve(this.caTrustInfo_);
  }

  importServerCertificate() {
    this.methodCalled('importServerCertificate');
    return Promise.resolve();
  }

  importCaCertificate() {
    this.methodCalled('importCaCertificate');
    return Promise.resolve('dummyName');
  }

  importCaCertificateTrustSelected(
      ssl: boolean, email: boolean, objSign: boolean) {
    this.methodCalled(
        'importCaCertificateTrustSelected',
        {ssl: ssl, email: email, objSign: objSign});
    return this.fulfillRequest_();
  }

  editCaCertificateTrust(
      id: string, ssl: boolean, email: boolean, objSign: boolean) {
    this.methodCalled('editCaCertificateTrust', {id, ssl, email, objSign});
    return this.fulfillRequest_();
  }

  /**
   * Forces some of the browser proxy methods to start returning errors.
   */
  forceCertificatesError() {
    this.certificatesError_ = {
      title: 'DummyError',
      description: 'DummyDescription',
    };
  }

  /**
   * @return A promise that is resolved or rejected based on the
   *     value of |certificatesError_|.
   */
  private fulfillRequest_(): Promise<void> {
    return this.certificatesError_ === null ?
        Promise.resolve() :
        Promise.reject(this.certificatesError_);
  }

  deleteCertificate(id: string) {
    this.methodCalled('deleteCertificate', id);
    return this.fulfillRequest_();
  }

  exportPersonalCertificatePasswordSelected(password: string) {
    this.methodCalled('exportPersonalCertificatePasswordSelected', password);
    return this.fulfillRequest_();
  }

  importPersonalCertificate(useHardwareBacked: boolean) {
    this.methodCalled('importPersonalCertificate', useHardwareBacked);
    return Promise.resolve(true);
  }

  importPersonalCertificatePasswordSelected(password: string) {
    this.methodCalled('importPersonalCertificatePasswordSelected', password);
    return this.fulfillRequest_();
  }

  refreshCertificates() {
    this.methodCalled('refreshCertificates');
  }

  viewCertificate(id: string) {
    this.methodCalled('viewCertificate', id);
  }

  exportCertificate(id: string) {
    this.methodCalled('exportCertificate', id);
  }

  exportPersonalCertificate(id: string) {
    this.methodCalled('exportPersonalCertificate', id);
    return Promise.resolve();
  }

  cancelImportExportCertificate() {}
}

function createSampleCertificateOrgGroup(): CertificatesOrgGroup {
  return {
    id: 'dummyCertificateId',
    name: 'dummyCertificateName',
    containsPolicyCerts: false,
    subnodes: [createSampleCertificateSubnode()],
  };
}

function createSampleCertificateSubnode(): CertificateSubnode {
  return {
    extractable: false,
    id: 'dummySubnodeId',
    name: 'dummySubnodeName',
    policy: false,
    canBeDeleted: true,
    canBeEdited: true,
    untrusted: false,
    webTrustAnchor: false,
  };
}

/**
 * Triggers an 'input' event on the given text input field (which triggers
 * validation to occur for password fields being tested in this file).
 */
async function triggerInputEvent(element: CrInputElement) {
  await element.updateComplete;
  // The actual key code is irrelevant for tests.
  const kSpaceBar = 32;
  keyEventOn(element, 'input', kSpaceBar);
  await element.updateComplete;
}

suite('CaTrustEditDialogTests', function() {
  let dialog: CaTrustEditDialogElement;

  let browserProxy: TestCertificatesBrowserProxy;

  const caTrustInfo: CaTrustInfo = {ssl: true, email: false, objSign: false};

  setup(async function() {
    browserProxy = new TestCertificatesBrowserProxy();
    browserProxy.setCaCertificateTrust(caTrustInfo);

    CertificatesBrowserProxyImpl.setInstance(browserProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    dialog = document.createElement('ca-trust-edit-dialog');
  });

  teardown(function() {
    dialog.remove();
  });

  test('EditSuccess', async function() {
    dialog.model = createSampleCertificateSubnode();
    document.body.appendChild(dialog);

    const id = await browserProxy.whenCalled('getCaCertificateTrust');

    assertEquals((dialog.model as CertificateSubnode).id, id);
    assertEquals(caTrustInfo.ssl, dialog.$.ssl.checked);
    assertEquals(caTrustInfo.email, dialog.$.email.checked);
    assertEquals(caTrustInfo.objSign, dialog.$.objSign.checked);

    // Simulate toggling all checkboxes.
    dialog.$.ssl.click();
    dialog.$.email.click();
    dialog.$.objSign.click();

    // Simulate clicking 'OK'.
    dialog.$.ok.click();

    const {id: model_id, ssl, email, objSign} =
        await browserProxy.whenCalled('editCaCertificateTrust');

    assertEquals((dialog.model as CertificateSubnode).id, model_id);
    // Checking that the values sent to C++ are reflecting the
    // changes made by the user (toggling all checkboxes).
    assertEquals(caTrustInfo.ssl, !ssl);
    assertEquals(caTrustInfo.email, !email);
    assertEquals(caTrustInfo.objSign, !objSign);
    // Check that the dialog is closed.
    assertFalse(dialog.$.dialog.open);
  });

  test('ImportSuccess', async function() {
    dialog.model = {name: 'Dummy certificate name'};
    document.body.appendChild(dialog);

    assertFalse(dialog.$.ssl.checked);
    assertFalse(dialog.$.email.checked);
    assertFalse(dialog.$.objSign.checked);

    dialog.$.ssl.click();
    dialog.$.email.click();

    // Simulate clicking 'OK'.
    dialog.$.ok.click();
    const {ssl, email, objSign} =
        await browserProxy.whenCalled('importCaCertificateTrustSelected');

    assertTrue(ssl);
    assertTrue(email);
    assertFalse(objSign);
  });

  test('EditError', async function() {
    dialog.model = createSampleCertificateSubnode();
    document.body.appendChild(dialog);
    browserProxy.forceCertificatesError();

    const whenErrorEventFired = eventToPromise('certificates-error', dialog);

    await browserProxy.whenCalled('getCaCertificateTrust');

    dialog.$.ok.click();
    await browserProxy.whenCalled('editCaCertificateTrust');

    await whenErrorEventFired;
  });
});

suite('CertificateDeleteConfirmationDialogTests', function() {
  let dialog: CertificateDeleteConfirmationDialogElement;
  let browserProxy: TestCertificatesBrowserProxy;

  const model = createSampleCertificateSubnode();

  setup(function() {
    browserProxy = new TestCertificatesBrowserProxy();
    CertificatesBrowserProxyImpl.setInstance(browserProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    dialog = document.createElement('certificate-delete-confirmation-dialog');
    dialog.model = model;
    dialog.certificateType = CertificateType.PERSONAL;
    document.body.appendChild(dialog);
  });

  teardown(function() {
    dialog.remove();
  });

  test('DeleteSuccess', async function() {
    assertTrue(dialog.$.dialog.open);
    // Check that the dialog title includes the certificate name.
    const titleEl = dialog.$.dialog.querySelector('[slot=title]');
    assertTrue(!!titleEl);
    assertTrue(titleEl.textContent!.includes(model.name));

    // Simulate clicking 'OK'.
    dialog.$.ok.click();

    const id = await browserProxy.whenCalled('deleteCertificate');
    assertEquals(model.id, id);
    // Check that the dialog is closed.
    assertFalse(dialog.$.dialog.open);
  });

  test('DeleteError', async function() {
    browserProxy.forceCertificatesError();
    const whenErrorEventFired = eventToPromise('certificates-error', dialog);

    // Simulate clicking 'OK'.
    dialog.$.ok.click();
    const id = await browserProxy.whenCalled('deleteCertificate');
    assertEquals(model.id, id);
    // Ensure that the 'error' event was fired.
    await whenErrorEventFired;
  });
});

suite('CertificatePasswordEncryptionDialogTests', function() {
  let dialog: CertificatePasswordEncryptionDialogElement;
  let browserProxy: TestCertificatesBrowserProxy;

  const methodName = 'exportPersonalCertificatePasswordSelected';

  setup(function() {
    browserProxy = new TestCertificatesBrowserProxy();
    CertificatesBrowserProxyImpl.setInstance(browserProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    dialog = document.createElement('certificate-password-encryption-dialog');
    document.body.appendChild(dialog);
  });

  teardown(function() {
    dialog.remove();
  });

  test('EncryptSuccess', async function() {
    const passwordInputElements = dialog.$.dialog.querySelectorAll('cr-input');
    const passwordInputElement = passwordInputElements[0];
    assertTrue(!!passwordInputElement);
    const confirmPasswordInputElement = passwordInputElements[1];
    assertTrue(!!confirmPasswordInputElement);

    assertTrue(dialog.$.dialog.open);
    assertTrue(dialog.$.ok.disabled);

    // Test that the 'OK' button is disabled when the password fields are
    // empty (even though they both have the same value).
    await triggerInputEvent(passwordInputElement);
    assertTrue(dialog.$.ok.disabled);

    // Test that the 'OK' button is disabled until the two password fields
    // match.
    passwordInputElement.value = 'foopassword';
    await triggerInputEvent(passwordInputElement);
    assertTrue(dialog.$.ok.disabled);
    confirmPasswordInputElement.value = passwordInputElement.value;
    await triggerInputEvent(confirmPasswordInputElement);
    assertFalse(dialog.$.ok.disabled);

    // Simulate clicking 'OK'.
    dialog.$.ok.click();

    const password = await browserProxy.whenCalled(methodName);
    assertEquals(passwordInputElement.value, password);
    // Check that the dialog is closed.
    assertFalse(dialog.$.dialog.open);
  });

  test('EncryptError', async function() {
    browserProxy.forceCertificatesError();

    const passwordInputElements = dialog.$.dialog.querySelectorAll('cr-input');
    const passwordInputElement = passwordInputElements[0];
    assertTrue(!!passwordInputElement);
    const confirmPasswordInputElement = passwordInputElements[1];
    assertTrue(!!confirmPasswordInputElement);

    passwordInputElement.value = 'foopassword';
    confirmPasswordInputElement.value = passwordInputElement.value;
    await triggerInputEvent(passwordInputElement);

    const whenErrorEventFired = eventToPromise('certificates-error', dialog);
    dialog.$.ok.click();

    await browserProxy.whenCalled(methodName);
    await whenErrorEventFired;
  });
});

suite('CertificatePasswordDecryptionDialogTests', function() {
  let dialog: CertificatePasswordDecryptionDialogElement;
  let browserProxy: TestCertificatesBrowserProxy;

  const methodName = 'importPersonalCertificatePasswordSelected';

  setup(function() {
    browserProxy = new TestCertificatesBrowserProxy();
    CertificatesBrowserProxyImpl.setInstance(browserProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    dialog = document.createElement('certificate-password-decryption-dialog');
    document.body.appendChild(dialog);
  });

  teardown(function() {
    dialog.remove();
  });

  test('DecryptSuccess', async function() {
    const passwordInputElement = dialog.$.dialog.querySelector('cr-input');
    assertTrue(!!passwordInputElement);
    assertTrue(dialog.$.dialog.open);

    // Test that the 'OK' button is enabled even when the password field is
    // empty.
    assertEquals('', passwordInputElement.value);
    assertFalse(dialog.$.ok.disabled);

    passwordInputElement.value = 'foopassword';
    await passwordInputElement.updateComplete;
    assertFalse(dialog.$.ok.disabled);

    // Simulate clicking 'OK'.
    dialog.$.ok.click();

    const password = await browserProxy.whenCalled(methodName);
    assertEquals(passwordInputElement.value, password);
    // Check that the dialog is closed.
    assertFalse(dialog.$.dialog.open);
  });

  test('DecryptError', async function() {
    browserProxy.forceCertificatesError();
    // Simulate entering some password.
    const passwordInputElement = dialog.$.dialog.querySelector('cr-input');
    assertTrue(!!passwordInputElement);
    passwordInputElement.value = 'foopassword';
    await triggerInputEvent(passwordInputElement);

    const whenErrorEventFired = eventToPromise('certificates-error', dialog);
    dialog.$.ok.click();
    await browserProxy.whenCalled(methodName);
    await whenErrorEventFired;
  });
});

suite('CertificateSubentryTests', function() {
  let subentry: CertificateSubentryElement;
  let browserProxy: TestCertificatesBrowserProxy;

  /**
   * @return A promise firing once |CertificateActionEvent| fires.
   */
  function actionEventToPromise():
      Promise<CustomEvent<CertificateActionEventDetail>> {
    return eventToPromise(CertificateActionEvent, subentry);
  }

  setup(function() {
    browserProxy = new TestCertificatesBrowserProxy();
    CertificatesBrowserProxyImpl.setInstance(browserProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    subentry = document.createElement('certificate-subentry');
    subentry.model = createSampleCertificateSubnode();
    subentry.certificateType = CertificateType.PERSONAL;
    document.body.appendChild(subentry);

    // Bring up the popup menu for the following tests to use.
    subentry.$.dots.click();
    flush();
  });

  teardown(function() {
    subentry.remove();
  });

  // Test case where 'View' option is tapped.
  test('MenuOptions_View', async function() {
    const viewButton = subentry.shadowRoot!.querySelector<HTMLElement>('#view');
    assertTrue(!!viewButton);
    viewButton.click();
    const id = await browserProxy.whenCalled('viewCertificate');
    assertEquals(subentry.model.id, id);
  });

  // Test that the 'Edit' option is only shown when appropriate and that
  // once tapped the correct event is fired.
  test('MenuOptions_Edit', function() {
    const editButton = subentry.shadowRoot!.querySelector<HTMLElement>('#edit');
    assertTrue(!!editButton);

    let model = createSampleCertificateSubnode();
    model.canBeEdited = false;
    subentry.model = model;
    assertTrue(editButton.hidden);

    model = createSampleCertificateSubnode();
    model.canBeEdited = true;
    subentry.model = model;
    assertFalse(editButton.hidden);

    subentry.model = createSampleCertificateSubnode();
    const waitForActionEvent = actionEventToPromise();
    editButton.click();
    return waitForActionEvent.then(function(event) {
      const detail = event.detail;
      assertEquals(CertificateAction.EDIT, detail.action);
      assertEquals(
          subentry.model.id, (detail.subnode as CertificateSubnode).id);
      assertEquals(subentry.certificateType, detail.certificateType);
    });
  });

  // Test that the 'Delete' option is only shown when appropriate and that
  // once tapped the correct event is fired.
  test('MenuOptions_Delete', function() {
    const deleteButton =
        subentry.shadowRoot!.querySelector<HTMLElement>('#delete');
    assertTrue(!!deleteButton);

    // Should be disabled when 'model.canBeDeleted' is false.
    const model = createSampleCertificateSubnode();
    model.canBeDeleted = false;
    subentry.model = model;
    assertTrue(deleteButton.hidden);

    subentry.model = createSampleCertificateSubnode();
    const waitForActionEvent = actionEventToPromise();
    deleteButton.click();
    return waitForActionEvent.then(function(event) {
      const detail = event.detail;
      assertEquals(CertificateAction.DELETE, detail.action);
      assertEquals(
          subentry.model.id, (detail.subnode as CertificateSubnode).id);
    });
  });

  // Test that the 'Export' option is always shown when the certificate type
  // is not PERSONAL and that once tapped the correct event is fired.
  test('MenuOptions_Export', async function() {
    subentry.certificateType = CertificateType.SERVER;
    const exportButton =
        subentry.shadowRoot!.querySelector<HTMLElement>('#export');
    assertTrue(!!exportButton);
    assertFalse(exportButton.hidden);
    exportButton.click();
    const id = await browserProxy.whenCalled('exportCertificate');
    assertEquals(subentry.model.id, id);
  });

  // Test case of exporting a PERSONAL certificate.
  test('MenuOptions_ExportPersonal', async function() {
    const exportButton =
        subentry.shadowRoot!.querySelector<HTMLElement>('#export');
    assertTrue(!!exportButton);

    // Should be disabled when 'model.extractable' is false.
    assertTrue(exportButton.hidden);

    const model = createSampleCertificateSubnode();
    model.extractable = true;
    subentry.model = model;
    assertFalse(exportButton.hidden);

    const waitForActionEvent = actionEventToPromise();
    exportButton.click();
    const id = await browserProxy.whenCalled('exportPersonalCertificate');

    assertEquals(subentry.model.id, id);

    // A promise firing once |CertificateActionEvent| is fired.
    const event = await waitForActionEvent;

    const detail = event.detail;
    assertEquals(CertificateAction.EXPORT_PERSONAL, detail.action);
    assertEquals(subentry.model.id, (detail.subnode as CertificateSubnode).id);
  });
});

suite('CertificateManagerTests', function() {
  let page: CertificateManagerElement;
  let browserProxy: TestCertificatesBrowserProxy;

  enum CertificateCategoryIndex {
    PERSONAL = 0,
    SERVER = 1,
    CA = 2,
    OTHER = 3,
  }

  setup(function() {
    browserProxy = new TestCertificatesBrowserProxy();
    CertificatesBrowserProxyImpl.setInstance(browserProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    page = document.createElement('certificate-manager');
    document.body.appendChild(page);
  });

  teardown(function() {
    page.remove();
  });

  /**
   * Test that the page requests information from the browser on startup and
   * that it gets populated accordingly.
   */
  test('Initialization', async function() {
    // Trigger all category tabs to be added to the DOM.
    const crTabsElement = page.shadowRoot!.querySelector('cr-tabs');
    assertTrue(!!crTabsElement);
    crTabsElement.selected = CertificateCategoryIndex.PERSONAL;
    await crTabsElement.updateComplete;
    crTabsElement.selected = CertificateCategoryIndex.SERVER;
    await crTabsElement.updateComplete;
    crTabsElement.selected = CertificateCategoryIndex.CA;
    await crTabsElement.updateComplete;
    crTabsElement.selected = CertificateCategoryIndex.OTHER;
    await crTabsElement.updateComplete;
    const certificateLists =
        page.shadowRoot!.querySelectorAll('certificate-list');
    assertEquals(4, certificateLists.length);

    async function assertCertificateListLength(
        listIndex: CertificateCategoryIndex, expectedSize: number) {
      // Need to switch to the corresponding tab before querying the DOM.
      assertTrue(!!crTabsElement);
      crTabsElement.selected = listIndex;
      await crTabsElement.updateComplete;
      const certificateEntries =
          certificateLists[listIndex]!.shadowRoot!.querySelectorAll(
              'certificate-entry');
      assertEquals(expectedSize, certificateEntries.length);
    }

    await assertCertificateListLength(CertificateCategoryIndex.PERSONAL, 0);
    await assertCertificateListLength(CertificateCategoryIndex.SERVER, 0);
    await assertCertificateListLength(CertificateCategoryIndex.CA, 0);
    await assertCertificateListLength(CertificateCategoryIndex.OTHER, 0);

    await browserProxy.whenCalled('refreshCertificates');
    // Simulate response for personal and CA certificates.
    webUIListenerCallback(
        'certificates-changed', 'personalCerts',
        [createSampleCertificateOrgGroup()]);
    webUIListenerCallback(
        'certificates-changed', 'caCerts',
        [createSampleCertificateOrgGroup(), createSampleCertificateOrgGroup()]);
    flush();

    assertCertificateListLength(CertificateCategoryIndex.PERSONAL, 1);
    assertCertificateListLength(CertificateCategoryIndex.SERVER, 0);
    assertCertificateListLength(CertificateCategoryIndex.CA, 2);
    assertCertificateListLength(CertificateCategoryIndex.OTHER, 0);
  });

  /**
   * Tests that a dialog opens as a response to a CertificateActionEvent.
   * @param dialogTagName The type of dialog to test.
   */
  function testDialogOpensOnAction(
      dialogTagName: string,
      eventDetail: CertificateActionEventDetail): Promise<void> {
    assertFalse(!!page.shadowRoot!.querySelector(dialogTagName));
    const whenDialogOpen = eventToPromise('cr-dialog-open', page);
    page.dispatchEvent(new CustomEvent(
        CertificateActionEvent,
        {bubbles: true, composed: true, detail: eventDetail}));

    // Some dialogs are opened after some async operation to fetch initial
    // data. Ensure that the underlying cr-dialog is actually opened before
    // returning.
    return whenDialogOpen.then(() => {
      assertTrue(!!page.shadowRoot!.querySelector(dialogTagName));
    });
  }

  test('OpensDialog_DeleteConfirmation', function() {
    return testDialogOpensOnAction('certificate-delete-confirmation-dialog', {
      action: CertificateAction.DELETE,
      subnode: createSampleCertificateSubnode(),
      certificateType: CertificateType.PERSONAL,
      anchor: page,
    });
  });

  test('OpensDialog_PasswordEncryption', function() {
    return testDialogOpensOnAction('certificate-password-encryption-dialog', {
      action: CertificateAction.EXPORT_PERSONAL,
      subnode: createSampleCertificateSubnode(),
      certificateType: CertificateType.PERSONAL,
      anchor: page,
    });
  });

  test('OpensDialog_PasswordDecryption', function() {
    return testDialogOpensOnAction('certificate-password-decryption-dialog', {
      action: CertificateAction.IMPORT,
      subnode: createSampleCertificateSubnode(),
      certificateType: CertificateType.PERSONAL,
      anchor: page,
    });
  });

  test('OpensDialog_CaTrustEdit', function() {
    return testDialogOpensOnAction('ca-trust-edit-dialog', {
      action: CertificateAction.EDIT,
      subnode: createSampleCertificateSubnode(),
      certificateType: CertificateType.CA,
      anchor: page,
    });
  });

  test('OpensDialog_CaTrustImport', function() {
    return testDialogOpensOnAction('ca-trust-edit-dialog', {
      action: CertificateAction.IMPORT,
      subnode: {name: 'Dummy Certificate Name', id: ''},
      certificateType: CertificateType.CA,
      anchor: page,
    });
  });

  // <if expr="chromeos_ash">

  async function renderTabContents() {
    const crTabs = page.shadowRoot!.querySelector('cr-tabs');
    assertTrue(!!crTabs);
    crTabs.selected = CertificateCategoryIndex.PERSONAL;
    await crTabs.updateComplete;
    crTabs.selected = CertificateCategoryIndex.CA;
    await crTabs.updateComplete;
  }

  // Test that import buttons are hidden by default.
  test('ImportButton_Default', async function() {
    await renderTabContents();
    const certificateLists =
        page.shadowRoot!.querySelectorAll('certificate-list');
    const clientImportButton = certificateLists[0]!.$.import;
    assertTrue(clientImportButton.hidden);
    const clientImportAndBindButton = certificateLists[0]!.$.importAndBind;
    assertTrue(clientImportAndBindButton.hidden);
    const caImportButton = certificateLists[1]!.$.import;
    assertTrue(caImportButton.hidden);
  });

  // Test that ClientCertificateManagementAllowed policy is applied to the
  // UI when management is allowed.
  test('ImportButton_ClientPolicyAllowed', async function() {
    await renderTabContents();
    const certificateLists =
        page.shadowRoot!.querySelectorAll('certificate-list');

    await browserProxy.whenCalled('refreshCertificates');
    webUIListenerCallback(
        'client-import-allowed-changed', true /* clientImportAllowed */);
    // Verify that import buttons are shown in the client certificate
    // tab.
    const clientImportButton = certificateLists[0]!.$.import;
    assertFalse(clientImportButton.hidden);
    const clientImportAndBindButton = certificateLists[0]!.$.importAndBind;
    assertFalse(clientImportAndBindButton.hidden);
    // Verify that import button is still hidden in the CA certificate
    // tab.
    const caImportButton = certificateLists[1]!.$.import;
    assertTrue(caImportButton.hidden);
  });

  // Test that ClientCertificateManagementAllowed policy is applied to the
  // UI when management is not allowed.
  test('ImportButton_ClientPolicyDisallowed', async function() {
    await renderTabContents();
    const certificateLists =
        page.shadowRoot!.querySelectorAll('certificate-list');

    await browserProxy.whenCalled('refreshCertificates');
    webUIListenerCallback(
        'client-import-allowed-changed', false /* clientImportAllowed */);
    // Verify that import buttons are still hidden in the client
    // certificate tab.
    const clientImportButton = certificateLists[0]!.$.import;
    assertTrue(clientImportButton.hidden);
    const clientImportAndBindButton = certificateLists[0]!.$.importAndBind;
    assertTrue(clientImportAndBindButton.hidden);
    // Verify that import button is still hidden in the CA certificate
    // tab.
    const caImportButton = certificateLists[1]!.$.import;
    assertTrue(caImportButton.hidden);
  });

  // Test that CACertificateManagementAllowed policy is applied to the
  // UI when management is allowed.
  test('ImportButton_CAPolicyAllowed', async function() {
    await renderTabContents();
    const certificateLists =
        page.shadowRoot!.querySelectorAll('certificate-list');

    await browserProxy.whenCalled('refreshCertificates');
    webUIListenerCallback(
        'ca-import-allowed-changed', true /* clientImportAllowed */);
    // Verify that import buttons are still hidden in the client
    // certificate tab.
    const clientImportButton = certificateLists[0]!.$.import;
    assertTrue(clientImportButton.hidden);
    const clientImportAndBindButton = certificateLists[0]!.$.importAndBind;
    assertTrue(clientImportAndBindButton.hidden);
    // Verify that import button is shown in the CA certificate tab.
    const caImportButton = certificateLists[1]!.$.import;
    assertFalse(caImportButton.hidden);
  });

  // Test that CACertificateManagementAllowed policy is applied to the
  // UI when management is not allowed.
  test('ImportButton_CAPolicyDisallowed', async function() {
    await renderTabContents();
    const certificateLists =
        page.shadowRoot!.querySelectorAll('certificate-list');

    await browserProxy.whenCalled('refreshCertificates');
    webUIListenerCallback(
        'ca-import-allowed-changed', false /* clientImportAllowed */);
    // Verify that import buttons are still hidden in the client
    // certificate tab.
    const clientImportButton = certificateLists[0]!.$.import;
    assertTrue(clientImportButton.hidden);
    const clientImportAndBindButton = certificateLists[0]!.$.importAndBind;
    assertTrue(clientImportAndBindButton.hidden);
    // Verify that import button is still hidden in the CA certificate
    // tab.
    const caImportButton = certificateLists[1]!.$.import;
    assertTrue(caImportButton.hidden);
  });
  // </if>
});

suite('CertificateListTests', function() {
  let element: CertificateListElement;
  let browserProxy: TestCertificatesBrowserProxy;

  setup(function() {
    browserProxy = new TestCertificatesBrowserProxy();
    CertificatesBrowserProxyImpl.setInstance(browserProxy);
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    element = document.createElement('certificate-list');
    document.body.appendChild(element);
  });

  teardown(function() {
    element.remove();
  });

  /**
   * Tests the "Import" button functionality.
   * @param proxyMethodName The name of the proxy method expected to be
   *     called.
   * @param actionEventExpected Whether a CertificateActionEvent is expected
   *     to fire as a result tapping the Import button.
   * @param bindBtn Whether to click on the import and bind btn.
   */
  async function testImportForCertificateType(
      certificateType: CertificateType, proxyMethodName: string,
      actionEventExpected: boolean, bindBtn: boolean) {
    element.certificateType = certificateType;
    flush();

    const importButton = bindBtn ?
        element.shadowRoot!.querySelector<HTMLElement>('#importAndBind') :
        element.shadowRoot!.querySelector<HTMLElement>('#import');
    assertTrue(!!importButton);

    const waitForActionEvent = actionEventExpected ?
        eventToPromise(CertificateActionEvent, element) :
        Promise.resolve(null);

    importButton.click();
    const arg = await browserProxy.whenCalled(proxyMethodName);

    if (proxyMethodName === 'importPersonalCertificate') {
      assertNotEquals(arg, undefined);
      assertEquals(arg, bindBtn);
    }
    const event = await waitForActionEvent;

    if (actionEventExpected) {
      assertEquals(CertificateAction.IMPORT, event.detail.action);
      assertEquals(certificateType, event.detail.certificateType);
    }
  }

  test('ImportButton_Personal', async function() {
    await testImportForCertificateType(
        CertificateType.PERSONAL, 'importPersonalCertificate', true, false);
  });

  // <if expr="chromeos_ash">
  test('ImportAndBindButton_Personal', async function() {
    await testImportForCertificateType(
        CertificateType.PERSONAL, 'importPersonalCertificate', true, true);
  });
  // </if>

  test('ImportButton_Server', async function() {
    await testImportForCertificateType(
        CertificateType.SERVER, 'importServerCertificate', false, false);
  });

  test('ImportButton_CA', async function() {
    await testImportForCertificateType(
        CertificateType.CA, 'importCaCertificate', true, false);
  });
});