chromium/chrome/test/data/webui/settings/payments_section_interactive_test.ts

// Copyright 2020 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://settings/lazy_load.js';

import {flush} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import type {CrButtonElement} from 'chrome://settings/settings.js';
import {loadTimeData} from 'chrome://settings/settings.js';
import type {CrInputElement, SettingsCreditCardEditDialogElement, SettingsIbanEditDialogElement, SettingsPaymentsSectionElement} from 'chrome://settings/lazy_load.js';
import {PaymentsManagerImpl} from 'chrome://settings/lazy_load.js';
import {assertEquals, assertFalse, assertNotEquals, assertTrue} from 'chrome://webui-test/chai_assert.js';
import {eventToPromise, isVisible, microtasksFinished, whenAttributeIs} from 'chrome://webui-test/test_util.js';
import {flushTasks} from 'chrome://webui-test/polymer_test_util.js';

import {createCreditCardEntry, createIbanEntry, TestPaymentsManager} from './autofill_fake_data.js';
// clang-format on

/**
 * Helper function to simulate typing in nickname in the nickname field.
 */
async function typeInNickname(
    nicknameInput: CrInputElement, nickname: string): Promise<void> {
  nicknameInput.value = nickname;
  await nicknameInput.updateComplete;
  nicknameInput.dispatchEvent(
      new CustomEvent('input', {bubbles: true, composed: true}));
}

/**
 * Helper function to wait for IBAN validation to complete and any associated UI
 * to be updated.
 */
async function ibanValidated(paymentsManager: TestPaymentsManager) {
  await paymentsManager.whenCalled('isValidIban');
  await microtasksFinished();
}

suite('PaymentsSectionCreditCardEditDialogTest', function() {
  setup(function() {
    document.body.innerHTML = window.trustedTypes!.emptyHTML;
    loadTimeData.overrideValues({
      showIbansSettings: true,
      cvcStorageAvailable: true,
    });
  });

  /**
   * Creates the payments section for the given credit card and IBAN list.
   */
  async function createPaymentsSection(
      creditCards: chrome.autofillPrivate.CreditCardEntry[],
      ibans: chrome.autofillPrivate.IbanEntry[]):
      Promise<SettingsPaymentsSectionElement> {
    // Override the PaymentsManagerImpl for testing.
    const paymentsManager = new TestPaymentsManager();
    paymentsManager.data.creditCards = creditCards;
    paymentsManager.data.ibans = ibans;
    PaymentsManagerImpl.setInstance(paymentsManager);

    const section = document.createElement('settings-payments-section');
    section.prefs = {
      autofill: {
        credit_card_enabled: {value: true},
        payment_methods_mandatory_reauth: {value: true},
        payment_cvc_storage: {value: true},
      },
    };
    document.body.appendChild(section);
    await flushTasks();
    return section;
  }

  /**
   * Creates the Add Credit Card dialog. Simulate clicking "Add" button in
   * payments section.
   */
  async function createAddCreditCardDialog():
      Promise<SettingsCreditCardEditDialogElement> {
    const section = await createPaymentsSection(
        /*creditCards=*/[], /*ibans=*/[]);
    // Simulate clicking "Add" button in payments section.
    assertFalse(!!section.shadowRoot!.querySelector(
        'settings-credit-card-edit-dialog'));
    const addCreditCardButton =
        section.shadowRoot!.querySelector<CrButtonElement>('#addCreditCard');
    assertTrue(!!addCreditCardButton);
    addCreditCardButton.click();
    flush();
    const creditCardDialog =
        section.shadowRoot!.querySelector('settings-credit-card-edit-dialog');
    assertTrue(!!creditCardDialog);
    return creditCardDialog;
  }

  /**
   * Creates the Add Credit Card dialog. Simulate clicking "Credit/Debit card"
   * option from dropdown list.
   */
  async function createAddCreditCardDialogFromDropdown():
      Promise<SettingsCreditCardEditDialogElement> {
    const section = await createPaymentsSection(
        /*creditCards=*/[], /*ibans=*/[]);
    // Simulate clicking "Add" button in payments section.
    assertFalse(!!section.shadowRoot!.querySelector(
        'settings-credit-card-edit-dialog'));
    const dropdownAddPaymentMethodsButton =
        section.shadowRoot!.querySelector<CrButtonElement>(
            '#addPaymentMethods');
    assertTrue(!!dropdownAddPaymentMethodsButton);
    dropdownAddPaymentMethodsButton.click();
    flush();

    // Simulate clicking the 'Credit/Debit card' option in the menu.
    const addCardOption =
        section.shadowRoot!.querySelector<CrButtonElement>('#addCreditCard');
    assertTrue(!!addCardOption);
    addCardOption.click();
    flush();
    const creditCardDialog =
        section.shadowRoot!.querySelector('settings-credit-card-edit-dialog');
    assertTrue(!!creditCardDialog);
    return creditCardDialog;
  }

  /**
   * Creates the Add IBAN dialog. Simulate clicking "IBAN" option from the
   * dropdown list.
   */
  async function createAddIbanDialogFromDropdown():
      Promise<SettingsIbanEditDialogElement> {
    const section = await createPaymentsSection(
        /*creditCards=*/[], /*ibans=*/[]);
    // Simulate clicking "Add" button in payments section.
    assertFalse(!!section.shadowRoot!.querySelector(
        'settings-credit-card-edit-dialog'));
    const addpaymentMethodsButton =
        section.shadowRoot!.querySelector<CrButtonElement>(
            '#addPaymentMethods');
    assertTrue(!!addpaymentMethodsButton);
    addpaymentMethodsButton.click();
    flush();

    // Simulate clicking the 'IBAN' option in the menu.
    const addIbanOption =
        section.shadowRoot!.querySelector<CrButtonElement>('#addIban');
    assertTrue(!!addIbanOption);
    addIbanOption.click();
    flush();
    const ibanDialog =
        section.shadowRoot!.querySelector('settings-iban-edit-dialog');
    assertTrue(!!ibanDialog);
    ibanDialog.$.saveButton.disabled = false;
    return ibanDialog!;
  }

  /**
   * Creates the Edit Credit Card dialog for existing local card by simulating
   * clicking three-dots menu button then clicking editing button of the first
   * card in the card list.
   */
  async function createEditCreditCardDialog(
      creditCards: chrome.autofillPrivate.CreditCardEntry[]):
      Promise<SettingsCreditCardEditDialogElement> {
    const section = await createPaymentsSection(creditCards, /*ibans=*/[]);
    // Simulate clicking three-dots menu button for the first card in the list.
    const rowShadowRoot =
        section.$.paymentsList.shadowRoot!
            .querySelector('settings-credit-card-list-entry')!.shadowRoot!;
    assertFalse(!!rowShadowRoot.querySelector('#remoteCreditCardLink'));
    const menuButton =
        rowShadowRoot.querySelector<HTMLElement>('#creditCardMenu');
    assertTrue(!!menuButton);
    menuButton.click();
    flush();

    // Simulate clicking the 'Edit' button in the menu.
    section.$.menuEditCreditCard.click();
    await flushTasks();
    const creditCardDialog =
        section.shadowRoot!.querySelector('settings-credit-card-edit-dialog');
    assertTrue(!!creditCardDialog);
    return creditCardDialog;
  }

  /**
   * Creates the Edit IBAN dialog for existing local IBANs by simulating
   * clicking the three-dots menu button then clicking the edit button of the
   * first IBAN in the list.
   */
  async function createEditIbanDialog(
      ibans: chrome.autofillPrivate.IbanEntry[]):
      Promise<SettingsIbanEditDialogElement> {
    const section = await createPaymentsSection(
        /*creditCards=*/[], /*ibans=*/ ibans);
    // Simulate clicking three-dots menu button for the first IBAN in the list.
    const firstEntry = section.$.paymentsList.shadowRoot!.querySelector(
        'settings-iban-list-entry');
    assertTrue(!!firstEntry);
    assertFalse(!!firstEntry.shadowRoot!.querySelector('#remoteIbanLink'));
    const menuButton =
        firstEntry.shadowRoot!.querySelector<HTMLElement>('#ibanMenu');
    assertTrue(!!menuButton);

    menuButton.click();
    flush();

    // Simulate clicking the 'Edit' button in the menu.
    const menuEditIban =
        section.shadowRoot!.querySelector<HTMLElement>('#menuEditIban');
    assertTrue(!!menuEditIban);
    menuEditIban.click();
    flush();
    const ibanDialog =
        section.shadowRoot!.querySelector('settings-iban-edit-dialog');
    assertTrue(!!ibanDialog);
    ibanDialog.$.saveButton.disabled = false;
    return ibanDialog;
  }

  function nextYear(): string {
    return (new Date().getFullYear() + 1).toString();
  }

  function farFutureYear(): string {
    return (new Date().getFullYear() + 15).toString();
  }

  function lastYear(): string {
    return (new Date().getFullYear() - 1).toString();
  }

  test('add card dialog', async function() {
    loadTimeData.overrideValues({
      showIbansSettings: false,
    });
    const creditCardDialog = await createAddCreditCardDialog();

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');

    const nicknameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nicknameInput');
    const nameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nameInput');
    const numberInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#numberInput');
    const cvcInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>('#cvcInput');

    // Verify the nickname input field is shown when nickname management is
    // enabled.
    assertTrue(!!nicknameInput);
    assertTrue(!!nameInput);
    assertTrue(!!numberInput);
    assertTrue(!!cvcInput);
    // Verify the card number field is autofocused when nickname management is
    // enabled.
    assertTrue(numberInput.matches(':focus-within'));
  });

  test('add card dialog from dropdown list', async function() {
    loadTimeData.overrideValues({
      showIbansSettings: true,
    });
    const creditCardDialog = await createAddCreditCardDialogFromDropdown();

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');

    const nicknameInput = creditCardDialog.$.nicknameInput;
    const nameInput = creditCardDialog.$.nameInput;
    const numberInput = creditCardDialog.$.numberInput;
    const cvcInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>('#cvcInput');

    // Verify the nickname input field is shown when nickname management is
    // enabled.
    assertTrue(!!nicknameInput);
    assertTrue(!!nameInput);
    assertTrue(!!numberInput);
    assertTrue(!!cvcInput);
    // Verify the card number field is autofocused when nickname management is
    // enabled.
    assertTrue(numberInput.matches(':focus-within'));
  });

  test('save new card', async function() {
    const creditCardDialog = await createAddCreditCardDialogFromDropdown();

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');

    // Fill in name, card number, expiration year, card nickname and CVC, and
    // trigger the on-input handler.
    const nameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nameInput');
    const numberInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#numberInput');
    const nicknameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nicknameInput');
    const yearInput =
        creditCardDialog.shadowRoot!.querySelector<HTMLSelectElement>('#year');
    const cvcInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>('#cvcInput');
    assertTrue(!!cvcInput);
    nameInput!.value = 'Jane Doe';
    numberInput!.value = '4111111111111111';
    await typeInNickname(nicknameInput!, 'Grocery Card');
    yearInput!.value = nextYear();
    yearInput!.dispatchEvent(new CustomEvent('change'));
    cvcInput.value = '123';
    await cvcInput.updateComplete;
    flush();

    const expiredError =
        creditCardDialog.shadowRoot!.querySelector<HTMLElement>(
            '#expiredError');
    assertEquals('hidden', getComputedStyle(expiredError!).visibility);

    const saveButton =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#saveButton');
    assertFalse(saveButton!.disabled);

    const savedPromise = eventToPromise('save-credit-card', creditCardDialog);
    saveButton!.click();
    const saveEvent = await savedPromise;

    // Verify the input values are correctly passed to save-credit-card.
    // guid is undefined when saving a new card.
    assertEquals(saveEvent.detail.guid, undefined);
    assertEquals(saveEvent.detail.name, 'Jane Doe');
    assertEquals(saveEvent.detail.cardNumber, '4111111111111111');
    assertEquals(saveEvent.detail.nickname, 'Grocery Card');
    assertEquals(saveEvent.detail.expirationYear, nextYear());
    assertEquals('123', saveEvent.detail.cvc);
  });

  test('trim credit card when save', async function() {
    const creditCardDialog = await createAddCreditCardDialogFromDropdown();

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');

    // Set expiration year, fill in name, card number, and card nickname with
    // leading and trailing whitespaces, and trigger the on-input handler.
    const nameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nameInput');
    assertTrue(!!nameInput);
    const numberInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#numberInput');
    assertTrue(!!numberInput);
    const nicknameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nicknameInput');
    const yearInput =
        creditCardDialog.shadowRoot!.querySelector<HTMLSelectElement>('#year');
    const cvcInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>('#cvcInput');
    assertTrue(!!cvcInput);
    nameInput.value = '  Jane Doe  \n';
    numberInput.value = ' 4111111111111111 ';
    await Promise.all([
      nameInput.updateComplete,
      numberInput.updateComplete,
      typeInNickname(nicknameInput!, 'Grocery Card'),
    ]);
    yearInput!.value = nextYear();
    yearInput!.dispatchEvent(new CustomEvent('change'));
    cvcInput.value = ' ';
    await cvcInput.updateComplete;
    flush();

    const expiredError =
        creditCardDialog.shadowRoot!.querySelector<HTMLElement>(
            '#expiredError');
    assertEquals('hidden', getComputedStyle(expiredError!).visibility);

    const saveButton =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#saveButton');
    assertFalse(saveButton!.disabled);

    const savedPromise = eventToPromise('save-credit-card', creditCardDialog);
    saveButton!.click();
    const saveEvent = await savedPromise;

    // Verify the input values are correctly passed to save-credit-card.
    // guid is undefined when saving a new card.
    assertEquals(saveEvent.detail.guid, undefined);
    assertEquals(saveEvent.detail.name, 'Jane Doe');
    assertEquals(saveEvent.detail.cardNumber, '4111111111111111');
    assertEquals(saveEvent.detail.nickname, 'Grocery Card');
    assertEquals(saveEvent.detail.expirationYear, nextYear());
    // Due to PCI compliance we don't check the structure or length of the CVC,
    // thus don't make any updates to the same.
    assertEquals(' ', saveEvent.detail.cvc);
  });

  test('update local card value', async function() {
    const creditCard = createCreditCardEntry();
    creditCard.name = 'Wrong name';
    creditCard.nickname = 'Shopping Card';
    // Set the expiration year to next year to avoid expired card.
    creditCard.expirationYear = nextYear();
    creditCard.cardNumber = '4444333322221111';
    creditCard.cvc = '123';
    const creditCardDialog = await createEditCreditCardDialog([creditCard]);

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');

    // For editing local card, verify displaying with existing value.
    const nameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nameInput');
    assertTrue(!!nameInput);
    const nicknameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nicknameInput');
    const numberInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#numberInput');
    assertTrue(!!numberInput);
    const yearInput =
        creditCardDialog.shadowRoot!.querySelector<HTMLSelectElement>('#year');
    const cvcInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>('#cvcInput');
    assertTrue(!!cvcInput);
    assertEquals(nameInput.value, 'Wrong name');
    assertEquals(nicknameInput!.value, 'Shopping Card');
    assertEquals(numberInput.value, '4444333322221111');
    assertEquals(yearInput!.value, nextYear());

    const expiredError =
        creditCardDialog.shadowRoot!.querySelector<HTMLElement>(
            '#expiredError');
    assertEquals('hidden', getComputedStyle(expiredError!).visibility);

    const saveButton =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#saveButton');
    assertFalse(saveButton!.disabled);

    // Update cardholder name, card number, expiration year and nickname, and
    // trigger the on-input handler.
    nameInput.value = 'Jane Doe';
    numberInput.value = '4111111111111111';
    await Promise.all([
      nameInput.updateComplete,
      numberInput.updateComplete,
      typeInNickname(nicknameInput!, 'Grocery Card'),
    ]);
    yearInput!.value = farFutureYear();
    yearInput!.dispatchEvent(new CustomEvent('change'));
    cvcInput.value = '098';
    await cvcInput.updateComplete;
    flush();

    const savedPromise = eventToPromise('save-credit-card', creditCardDialog);
    saveButton!.click();
    const saveEvent = await savedPromise;

    // Verify the updated values are correctly passed to save-credit-card.
    assertEquals(saveEvent.detail.guid, creditCard.guid);
    assertEquals(saveEvent.detail.name, 'Jane Doe');
    assertEquals(saveEvent.detail.cardNumber, '4111111111111111');
    assertEquals(saveEvent.detail.nickname, 'Grocery Card');
    assertEquals(saveEvent.detail.expirationYear, farFutureYear());
    assertEquals('098', saveEvent.detail.cvc);
  });

  test('show error message when input nickname is invalid', async function() {
    const creditCardDialog = await createAddCreditCardDialogFromDropdown();

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');

    // User clicks on nickname input.
    const nicknameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nicknameInput');
    assertTrue(!!nicknameInput);
    nicknameInput.focus();

    const validInputs = [
      '',
      ' ',
      '~@#$%^&**(){}|<>',
      'Grocery Card',
      'Two percent Cashback',
      /* UTF-16 hex encoded credit card emoji */ 'Chase Freedom \uD83D\uDCB3',
    ];
    for (const nickname of validInputs) {
      await typeInNickname(nicknameInput, nickname);
      assertFalse(nicknameInput.invalid);
      // Error message is hidden for valid nickname input.
      assertEquals(
          'hidden', getComputedStyle(nicknameInput.$.error).visibility);
    }

    // Verify invalid nickname inputs.
    const invalidInputs = [
      '12345',
      '2abc',
      'abc3',
      'abc4de',
      'a 1 b',
      /* UTF-16 hex encoded digt 7 emoji */ 'Digit emoji: \u0037\uFE0F\u20E3',
    ];
    for (const nickname of invalidInputs) {
      await typeInNickname(nicknameInput, nickname);
      assertTrue(nicknameInput.invalid);
      assertNotEquals('', nicknameInput.errorMessage);
      // Error message is shown for invalid nickname input.
      assertEquals(
          'visible', getComputedStyle(nicknameInput.$.error).visibility);
    }
    // The error message is still shown even when user does not focus on the
    // nickname field.
    nicknameInput.blur();
    await nicknameInput.updateComplete;
    assertTrue(nicknameInput.invalid);
    assertEquals('visible', getComputedStyle(nicknameInput.$.error).visibility);
  });

  test('disable save button when input nickname is invalid', async function() {
    const creditCard = createCreditCardEntry();
    creditCard.name = 'Wrong name';
    // Set the expiration year to next year to avoid expired card.
    creditCard.expirationYear = nextYear();
    creditCard.cardNumber = '4444333322221111';
    // Edit dialog for an existing card with no nickname.
    const creditCardDialog = await createEditCreditCardDialog([creditCard]);

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');
    // Save button is enabled for existing card with no nickname.
    const saveButton =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#saveButton');
    assertFalse(saveButton!.disabled);
    const nicknameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nicknameInput');

    await typeInNickname(nicknameInput!, 'invalid: 123');
    // Save button is disabled since the nickname is invalid.
    assertTrue(saveButton!.disabled);

    await typeInNickname(nicknameInput!, 'valid nickname');
    // Save button is back to enabled since user updates with a valid nickname.
    assertFalse(saveButton!.disabled);
  });

  test('only show nickname character count when focused', async function() {
    const creditCardDialog = await createAddCreditCardDialogFromDropdown();

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');

    const nicknameInput =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#nicknameInput');
    assertTrue(!!nicknameInput);
    const characterCount =
        creditCardDialog.shadowRoot!.querySelector<HTMLElement>('#charCount')!;
    // Character count is not shown when add card dialog is open (not focusing
    // on nickname input field).
    assertFalse(isVisible(characterCount));

    // User clicks on nickname input.
    nicknameInput!.focus();
    // Character count is shown when nickname input field is focused.
    assertTrue(isVisible(characterCount));
    // For new card, the nickname is unset.
    assertTrue(characterCount.textContent!.includes('0/25'));

    // User types in one character. Ensure the character count is dynamically
    // updated.
    await typeInNickname(nicknameInput!, 'a');
    assertTrue(characterCount.textContent!.includes('1/25'));
    // User types in total 5 characters.
    await typeInNickname(nicknameInput!, 'abcde');
    assertTrue(characterCount.textContent!.includes('5/25'));

    // User click outside of nickname input, the character count isn't shown.
    nicknameInput!.blur();
    await nicknameInput.updateComplete;
    assertFalse(isVisible(characterCount));

    // User clicks on nickname input again.
    nicknameInput!.focus();
    await nicknameInput.updateComplete;
    // Character count is shown when nickname input field is re-focused.
    assertTrue(isVisible(characterCount));
    assertTrue(characterCount.textContent!.includes('5/25'));
  });

  test('expired card', async function() {
    const creditCard = createCreditCardEntry();
    // Set the expiration year to the previous year to simulate expired card.
    creditCard.expirationYear = lastYear();
    // Edit dialog for an existing card with no nickname.
    const creditCardDialog = await createEditCreditCardDialog([creditCard]);

    // Wait for the dialog to open.
    await whenAttributeIs(creditCardDialog.$.dialog, 'open', '');

    // Verify save button is disabled for expired credit card.
    const saveButton =
        creditCardDialog.shadowRoot!.querySelector<CrInputElement>(
            '#saveButton');
    const expiredError =
        creditCardDialog.shadowRoot!.querySelector<HTMLElement>(
            '#expiredError');
    // The expired error message is shown.
    assertEquals('visible', getComputedStyle(expiredError!).visibility);
    // Check a11y attributes added for correct error announcement.
    assertEquals('alert', expiredError!.getAttribute('role'));

    const monthInput =
        creditCardDialog.shadowRoot!.querySelector<HTMLSelectElement>('#month');
    const yearInput =
        creditCardDialog.shadowRoot!.querySelector<HTMLSelectElement>('#year');
    for (const select of [monthInput!, yearInput!]) {
      assertEquals('true', select.getAttribute('aria-invalid'));
      assertEquals(expiredError!.id, select.getAttribute('aria-errormessage'));
    }

    // Update the expiration year to next year to avoid expired card.
    yearInput!.value = nextYear();
    yearInput!.dispatchEvent(new CustomEvent('change'));
    flush();

    // Expired error message is hidden for valid expiration date.
    assertEquals('hidden', getComputedStyle(expiredError!).visibility);
    assertFalse(saveButton!.disabled);
    // Check a11y attributes for expiration error removed.
    assertEquals(null, expiredError!.getAttribute('role'));
    for (const select of [monthInput!, yearInput!]) {
      assertEquals('false', select.getAttribute('aria-invalid'));
      assertEquals(null, select.getAttribute('aria-errormessage'));
    }
  });

  test('add iban dialog from dropdown list', async function() {
    loadTimeData.overrideValues({
      showIbansSettings: true,
    });
    const ibanDialog = await createAddIbanDialogFromDropdown();

    // Wait for the dialog to open.
    await whenAttributeIs(ibanDialog.$.dialog, 'open', '');

    const nicknameInput = ibanDialog.$.nicknameInput;
    const valueInput = ibanDialog.$.valueInput;

    // Verify the value and nickname input fields are shown.
    assertTrue(!!valueInput);
    assertTrue(!!nicknameInput);
  });

  test('save new IBAN', async function() {
    loadTimeData.overrideValues({
      showIbansSettings: true,
    });
    const ibanDialog = await createAddIbanDialogFromDropdown();

    // Wait for the dialog to open.
    await whenAttributeIs(ibanDialog.$.dialog, 'open', '');

    const nicknameInput = ibanDialog.$.nicknameInput;
    const valueInput = ibanDialog.$.valueInput;
    const characterCount =
        ibanDialog.shadowRoot!.querySelector<HTMLElement>('#charCount');

    assertTrue(!!characterCount);
    assertFalse(isVisible(characterCount));
    // User clicks on nickname input.
    nicknameInput!.focus();
    // Character count is shown when nickname input field is focused.
    assertTrue(isVisible(characterCount));
    // For new IBAN, the nickname is unset.
    assertTrue(characterCount.textContent!.includes('0/25'));

    // Fill in IBAN value and nickname, and trigger the on-input handler.
    nicknameInput.value = 'My doctor\'s IBAN';
    await nicknameInput.updateComplete;
    assertTrue(characterCount.textContent!.includes('16/25'));

    valueInput.value = 'IT60X0542811101000000123456';

    // IBAN validation is asynchronous, so wait for it to complete and the save
    // button state to be updated.
    const paymentsManager =
        PaymentsManagerImpl.getInstance() as TestPaymentsManager;
    await ibanValidated(paymentsManager);

    const savedPromise = eventToPromise('save-iban', ibanDialog);
    const saveButton = ibanDialog.$.saveButton;
    saveButton!.click();
    const saveEvent = await savedPromise;

    // Verify the input values are correctly passed to save-iban.
    // `guid` is undefined when saving a new IBAN.
    assertEquals(saveEvent.detail.guid, undefined);
    assertEquals(saveEvent.detail.value, 'IT60X0542811101000000123456');
    assertEquals(saveEvent.detail.nickname, 'My doctor\'s IBAN');
  });

  test('trim IBAN when saving', async function() {
    loadTimeData.overrideValues({
      showIbansSettings: true,
    });
    const ibanDialog = await createAddIbanDialogFromDropdown();

    // Wait for the dialog to open.
    await whenAttributeIs(ibanDialog.$.dialog, 'open', '');

    // Fill in IBAN value and nickname, and trigger the on-input handler.
    const nicknameInput = ibanDialog.$.nicknameInput;
    const valueInput = ibanDialog.$.valueInput;
    nicknameInput.value = '   My doctor\'s IBAN  ';
    valueInput.value = '  IT60 X054 2811 1010 0000 0123 456 ';

    // IBAN validation is asynchronous, so wait for it to complete and the save
    // button state to be updated.
    const paymentsManager =
        PaymentsManagerImpl.getInstance() as TestPaymentsManager;
    await ibanValidated(paymentsManager);

    const savedPromise = eventToPromise('save-iban', ibanDialog);
    const saveButton = ibanDialog.$.saveButton;
    saveButton.click();
    const saveEvent = await savedPromise;

    // Verify the input values are correctly passed to save-iban.
    // `guid` is undefined when saving a new IBAN.
    assertEquals(saveEvent.detail.guid, undefined);
    assertEquals(saveEvent.detail.value, 'IT60X0542811101000000123456');
    assertEquals(saveEvent.detail.nickname, 'My doctor\'s IBAN');
  });

  test('update local IBAN value', async function() {
    loadTimeData.overrideValues({
      showIbansSettings: true,
    });
    const iban =
        createIbanEntry('IE64 IRCE 9205 0112 3456 78', 'My teacher\'s IBAN');
    const ibanDialog = await createEditIbanDialog([iban]);

    // Wait for the dialog to open.
    await whenAttributeIs(ibanDialog.$.dialog, 'open', '');

    // Update IBAN value and nickname, and trigger the on-input handler.
    const nicknameInput = ibanDialog.$.nicknameInput;
    const valueInput = ibanDialog.$.valueInput;
    valueInput.value = 'DE75 5121 0800 1245 1261 99';
    nicknameInput.value = 'My brother\'s IBAN';

    // IBAN validation is asynchronous, so wait for it to complete and the save
    // button state to be updated.
    const paymentsManager =
        PaymentsManagerImpl.getInstance() as TestPaymentsManager;
    await ibanValidated(paymentsManager);

    const savedPromise = eventToPromise('save-iban', ibanDialog);
    const saveButton = ibanDialog.$.saveButton;
    saveButton.click();
    const saveEvent = await savedPromise;

    // Verify the updated values are correctly passed to save-iban.
    assertEquals(saveEvent.detail.guid, iban.guid);
    assertEquals(saveEvent.detail.value, 'DE75512108001245126199');
    assertEquals(saveEvent.detail.nickname, 'My brother\'s IBAN');
  });

});