browser/components/payments/test/mochitest/test_basic_card_form.html
author Matthew Noorenberghe <mozilla@noorenberghe.ca>
Mon, 24 Sep 2018 19:51:39 +0000
changeset 437937 32d9dbdfae6b74ee1e1144fc65b8ab1029cb1eff
parent 437724 75036af7d851f0fd893147f22aaab43483d6b65e
child 438562 4ff0fcd61c7a1f791414516a6e22efca94530f19
permissions -rw-r--r--
Bug 1480717 - Fix credit card form billing address and persist checkbox layout. r=sfoster Differential Revision: https://phabricator.services.mozilla.com/D4175

<!DOCTYPE HTML>
<html>
<!--
Test the basic-card-form element
-->
<head>
  <meta charset="utf-8">
  <title>Test the basic-card-form element</title>
  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"></script>
  <script type="application/javascript" src="/tests/SimpleTest/AddTask.js"></script>
  <script src="sinon-2.3.2.js"></script>
  <script src="payments_common.js"></script>
  <script src="../../res/vendor/custom-elements.min.js"></script>
  <script src="../../res/unprivileged-fallbacks.js"></script>
  <script src="autofillEditForms.js"></script>

  <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
  <link rel="stylesheet" type="text/css" href="../../res/paymentRequest.css"/>
  <link rel="stylesheet" type="text/css" href="../../res/components/accepted-cards.css"/>
</head>
<body>
  <p id="display" style="height: 100vh; margin: 0;">
    <iframe id="templateFrame" src="../../res/paymentRequest.xhtml" width="0" height="0"
            style="float: left;"></iframe>
  </p>
<div id="content" style="display: none">

</div>
<pre id="test">
</pre>
<script type="module">
/** Test the basic-card-form element **/

/* global sinon */

import BasicCardForm from "../../res/containers/basic-card-form.js";

let display = document.getElementById("display");

function checkCCForm(customEl, expectedCard) {
  const CC_PROPERTY_NAMES = [
    "billingAddressGUID",
    "cc-number",
    "cc-name",
    "cc-exp-month",
    "cc-exp-year",
    "cc-type",
  ];
  for (let propName of CC_PROPERTY_NAMES) {
    let expectedVal = expectedCard[propName] || "";
    is(document.getElementById(propName).value,
       expectedVal.toString(),
       `Check ${propName}`);
  }
}

add_task(async function setup_once() {
  let templateFrame = document.getElementById("templateFrame");
  await SimpleTest.promiseFocus(templateFrame.contentWindow);
  let displayEl = document.getElementById("display");
  importDialogDependencies(templateFrame, displayEl);
});

add_task(async function test_initialState() {
  let form = new BasicCardForm();
  let {page} = form.requestStore.getState();
  is(page.id, "payment-summary", "Check initial page");
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();
  is(page.id, "payment-summary", "Check initial page after appending");

  // :-moz-ui-invalid, unlike :invalid, only applies to fields showing the error outline.
  let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
  is(fieldsVisiblyInvalid.length, 0, "Check no fields are visibly invalid on an empty 'add' form");

  form.remove();
});

add_task(async function test_backButton() {
  let form = new BasicCardForm();
  form.dataset.backButtonLabel = "Back";
  form.dataset.addBasicCardTitle = "Sample page title 2";
  await form.requestStore.setState({
    page: {
      id: "basic-card-page",
    },
    "basic-card-page": {
    },
  });
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  let stateChangePromise = promiseStateChange(form.requestStore);
  is(form.pageTitleHeading.textContent, "Sample page title 2", "Check title");
  is(form.backButton.textContent, "Back", "Check label");
  form.backButton.scrollIntoView();
  synthesizeMouseAtCenter(form.backButton, {});

  let {page} = await stateChangePromise;
  is(page.id, "payment-summary", "Check initial page after appending");

  form.remove();
});

add_task(async function test_saveButton() {
  let form = new BasicCardForm();
  form.dataset.addButtonLabel = "Add";
  form.dataset.errorGenericSave = "Generic error";
  await form.promiseReady;
  display.appendChild(form);

  let address1 = deepClone(PTU.Addresses.TimBL);
  address1.guid = "TimBLGUID";
  let address2 = deepClone(PTU.Addresses.TimBL2);
  address2.guid = "TimBL2GUID";

  await form.requestStore.setState({
    savedAddresses: {
      [address1.guid]: deepClone(address1),
      [address2.guid]: deepClone(address2),
    },
  });

  await asyncElementRendered();

  ok(!form.acceptedCardsList.hidden, "Accepted card list should be visible when adding a card");

  ok(form.saveButton.disabled, "Save button should initially be disabled");
  fillField(form.form.querySelector("#cc-number"), "4111 1111-1111 1111");
  form.form.querySelector("#cc-name").focus();
  // Check .disabled after .focus() so that it's after both "input" and "change" events.
  ok(form.saveButton.disabled, "Save button should still be disabled without a name");
  sendString("J. Smith");
  fillField(form.form.querySelector("#cc-exp-month"), "11");
  let year = (new Date()).getFullYear().toString();
  fillField(form.form.querySelector("#cc-exp-year"), year);
  fillField(form.form.querySelector("#cc-type"), "visa");
  isnot(form.form.querySelector("#billingAddressGUID").value, address2.guid,
        "Check initial billing address");
  fillField(form.form.querySelector("#billingAddressGUID"), address2.guid);
  is(form.form.querySelector("#billingAddressGUID").value, address2.guid,
     "Check selected billing address");
  form.saveButton.focus();
  ok(!form.saveButton.disabled,
     "Save button should be enabled since the required fields are filled");

  fillField(form.form.querySelector("#cc-exp-month"), "");
  fillField(form.form.querySelector("#cc-exp-year"), "");
  form.saveButton.focus();
  ok(form.saveButton.disabled,
     "Save button should be disabled since the required fields are empty");
  fillField(form.form.querySelector("#cc-exp-month"), "11");
  fillField(form.form.querySelector("#cc-exp-year"), year);
  form.saveButton.focus();
  ok(!form.saveButton.disabled,
     "Save button should be enabled since the required fields are filled again");

  info("blanking the cc-number field");
  fillField(form.form.querySelector("#cc-number"), "");
  ok(form.saveButton.disabled, "Save button is disabled after blanking cc-number");
  form.form.querySelector("#cc-number").blur();
  let fieldsVisiblyInvalid = form.querySelectorAll(":-moz-ui-invalid");
  is(fieldsVisiblyInvalid.length, 1, "Check 1 field visibly invalid after blanking and blur");
  is(fieldsVisiblyInvalid[0].id, "cc-number", "Check #cc-number is visibly invalid");

  fillField(form.form.querySelector("#cc-number"), "4111 1111-1111 1111");
  is(form.querySelectorAll(":-moz-ui-invalid").length, 0, "Check no fields visibly invalid");
  ok(!form.saveButton.disabled, "Save button is enabled after re-filling cc-number");

  let messagePromise = promiseContentToChromeMessage("updateAutofillRecord");
  is(form.saveButton.textContent, "Add", "Check label");
  form.saveButton.scrollIntoView();
  synthesizeMouseAtCenter(form.saveButton, {});

  let details = await messagePromise;
  ok(typeof(details.messageID) == "number" && details.messageID > 0, "Check messageID type");
  delete details.messageID;
  is(details.collectionName, "creditCards", "Check collectionName");
  isDeeply(details, {
    collectionName: "creditCards",
    guid: undefined,
    messageType: "updateAutofillRecord",
    record: {
      "cc-exp-month": "11",
      "cc-exp-year": year,
      "cc-name": "J. Smith",
      "cc-number": "4111 1111-1111 1111",
      "cc-type": "visa",
      "billingAddressGUID": address2.guid,
      "isTemporary": true,
    },
  }, "Check event details for the message to chrome");
  form.remove();
});

add_task(async function test_requiredAttributePropagated() {
  let form = new BasicCardForm();
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
  is(requiredElements.length, 6, "Number of required elements");
  for (let element of requiredElements) {
    if (element.id == "billingAddressGUID") {
      // The billing address has a different layout.
      continue;
    }
    let container = element.closest("label") || element.closest("div");
    ok(container.hasAttribute("required"), "Container should also be marked as required");
  }
  // Now test that toggling the `required` attribute will affect the container.
  let sampleRequiredElement = requiredElements[0];
  let sampleRequiredContainer = sampleRequiredElement.closest("label") ||
                                sampleRequiredElement.closest("div");
  sampleRequiredElement.removeAttribute("required");
  await form.requestStore.setState({});
  await asyncElementRendered();
  ok(!sampleRequiredElement.hasAttribute("required"),
     `"required" attribute should still be removed from element (${sampleRequiredElement.id})`);
  ok(!sampleRequiredContainer.hasAttribute("required"),
     `"required" attribute should be removed from container`);
  sampleRequiredElement.setAttribute("required", "true");
  await form.requestStore.setState({});
  await asyncElementRendered();
  ok(sampleRequiredContainer.hasAttribute("required"),
     "`required` attribute is re-added to container");

  form.remove();
});

add_task(async function test_genericError() {
  let form = new BasicCardForm();
  await form.requestStore.setState({
    page: {
      id: "test-page",
      error: "Generic Error",
    },
  });
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  ok(!isHidden(form.genericErrorText), "Error message should be visible");
  is(form.genericErrorText.textContent, "Generic Error", "Check error message");
  form.remove();
});

add_task(async function test_add_selectedShippingAddress() {
  let form = new BasicCardForm();
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  info("have an existing card in storage");
  let card1 = deepClone(PTU.BasicCards.JohnDoe);
  card1.guid = "9864798564";
  card1["cc-exp-year"] = 2011;

  let address1 = deepClone(PTU.Addresses.TimBL);
  address1.guid = "TimBLGUID";
  let address2 = deepClone(PTU.Addresses.TimBL2);
  address2.guid = "TimBL2GUID";

  await form.requestStore.setState({
    page: {
      id: "basic-card-page",
    },
    savedAddresses: {
      [address1.guid]: deepClone(address1),
      [address2.guid]: deepClone(address2),
    },
    savedBasicCards: {
      [card1.guid]: deepClone(card1),
    },
    selectedShippingAddress: address2.guid,
  });
  await asyncElementRendered();
  checkCCForm(form, {
    billingAddressGUID: address2.guid,
  });

  form.remove();
  await form.requestStore.reset();
});

add_task(async function test_add_noSelectedShippingAddress() {
  let form = new BasicCardForm();
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  info("have an existing card in storage but unused");
  let card1 = deepClone(PTU.BasicCards.JohnDoe);
  card1.guid = "9864798564";
  card1["cc-exp-year"] = 2011;

  let address1 = deepClone(PTU.Addresses.TimBL);
  address1.guid = "TimBLGUID";

  await form.requestStore.setState({
    page: {
      id: "basic-card-page",
    },
    savedAddresses: {
      [address1.guid]: deepClone(address1),
    },
    savedBasicCards: {
      [card1.guid]: deepClone(card1),
    },
    selectedShippingAddress: null,
  });
  await asyncElementRendered();
  checkCCForm(form, {
    billingAddressGUID: address1.guid,
  });

  info("now test with a missing selectedShippingAddress");
  await form.requestStore.setState({
    selectedShippingAddress: "some-missing-guid",
  });
  await asyncElementRendered();
  checkCCForm(form, {
    billingAddressGUID: address1.guid,
  });

  form.remove();
  await form.requestStore.reset();
});

add_task(async function test_edit() {
  let form = new BasicCardForm();
  form.dataset.updateButtonLabel = "Update";
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  let address1 = deepClone(PTU.Addresses.TimBL);
  address1.guid = "TimBLGUID";

  info("test year before current");
  let card1 = deepClone(PTU.BasicCards.JohnDoe);
  card1.guid = "9864798564";
  card1["cc-exp-year"] = 2011;
  card1.billingAddressGUID = address1.guid;

  await form.requestStore.setState({
    page: {
      id: "basic-card-page",
    },
    "basic-card-page": {
      guid: card1.guid,
    },
    savedAddresses: {
      [address1.guid]: deepClone(address1),
    },
    savedBasicCards: {
      [card1.guid]: deepClone(card1),
    },
  });
  await asyncElementRendered();
  is(form.saveButton.textContent, "Update", "Check label");
  is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
     "Check no fields are visibly invalid on an 'edit' form with a complete card");
  checkCCForm(form, card1);
  ok(!form.saveButton.disabled, "Save button should be enabled upon edit for a valid card");
  ok(form.acceptedCardsList.hidden, "Accepted card list should be hidden when editing a card");

  let requiredElements = [...form.form.elements].filter(e => e.required && !e.disabled);
  ok(requiredElements.length, "There should be at least one required element");
  is(requiredElements.length, 5, "Number of required elements");
  for (let element of requiredElements) {
    if (element.id == "billingAddressGUID") {
      // The billing address has a different layout.
      continue;
    }

    let container = element.closest("label") || element.closest("div");
    ok(element.hasAttribute("required"), "Element should be marked as required");
    ok(container.hasAttribute("required"), "Container should also be marked as required");
  }

  info("test future year");
  card1["cc-exp-year"] = 2100;

  await form.requestStore.setState({
    savedBasicCards: {
      [card1.guid]: deepClone(card1),
    },
  });
  await asyncElementRendered();
  checkCCForm(form, card1);

  info("test change to minimal record");
  let minimalCard = {
    // no expiration date or name
    "cc-number": "1234567690123",
    guid: "9gnjdhen46",
  };
  await form.requestStore.setState({
    page: {
      id: "basic-card-page",
    },
    "basic-card-page": {
      guid: minimalCard.guid,
    },
    savedBasicCards: {
      [minimalCard.guid]: deepClone(minimalCard),
    },
  });
  await asyncElementRendered();
  ok(form.querySelectorAll(":-moz-ui-invalid").length > 0,
     "Check fields are visibly invalid on an 'edit' form with missing fields");
  checkCCForm(form, minimalCard);

  info("change to no selected card");
  await form.requestStore.setState({
    page: {
      id: "basic-card-page",
    },
    "basic-card-page": {
      guid: null,
    },
  });
  await asyncElementRendered();
  is(form.querySelectorAll(":-moz-ui-invalid").length, 0,
     "Check no fields are visibly invalid after converting to an 'add' form");
  checkCCForm(form, {
    billingAddressGUID: address1.guid, // Default selected
  });

  form.remove();
});

add_task(async function test_field_validity_updates() {
  let form = new BasicCardForm();
  form.dataset.updateButtonLabel = "Add";
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  let ccNumber = form.form.querySelector("#cc-number");
  let nameInput = form.form.querySelector("#cc-name");
  let typeInput = form.form.querySelector("#cc-type");
  let monthInput = form.form.querySelector("#cc-exp-month");
  let yearInput = form.form.querySelector("#cc-exp-year");

  info("test with valid cc-number but missing cc-name");
  fillField(ccNumber, "4111111111111111");
  ok(ccNumber.checkValidity(), "cc-number field is valid with good input");
  ok(!nameInput.checkValidity(), "cc-name field is invalid when empty");
  ok(form.saveButton.disabled, "Save button should be disabled with incomplete input");

  info("correct by adding cc-name and expiration values");
  fillField(nameInput, "First");
  fillField(monthInput, "11");
  let year = (new Date()).getFullYear().toString();
  fillField(yearInput, year);
  fillField(typeInput, "visa");
  ok(ccNumber.checkValidity(), "cc-number field is valid with good input");
  ok(nameInput.checkValidity(), "cc-name field is valid with a value");
  ok(monthInput.checkValidity(), "cc-exp-month field is valid with a value");
  ok(yearInput.checkValidity(), "cc-exp-year field is valid with a value");
  ok(typeInput.checkValidity(), "cc-type field is valid with a value");
  ok(!form.saveButton.disabled, "Save button should not be disabled with good input");

  info("edit to make the cc-number invalid");
  ccNumber.focus();
  sendString("aa");
  nameInput.focus();
  sendString("Surname");

  ok(!ccNumber.checkValidity(), "cc-number field becomes invalid with bad input");
  ok(form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is visibly invalid");
  ok(nameInput.checkValidity(), "cc-name field is valid with a value");
  ok(form.saveButton.disabled, "Save button becomes disabled with bad input");

  info("fix the cc-number to make it all valid again");
  ccNumber.focus();
  sendKey("BACK_SPACE");
  sendKey("BACK_SPACE");
  info("after backspaces, ccNumber.value: " + ccNumber.value);

  ok(ccNumber.checkValidity(), "cc-number field becomes valid with corrected input");
  ok(nameInput.checkValidity(), "cc-name field is valid with a value");
  ok(!form.saveButton.disabled, "Save button is no longer disabled with corrected input");

  form.remove();
});

add_task(async function test_numberCustomValidityReset() {
  let form = new BasicCardForm();
  form.dataset.updateButtonLabel = "Add";
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  fillField(form.querySelector("#cc-number"), "junk");
  sendKey("TAB");
  ok(form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is visibly invalid");

  info("simulate triggering an add again to reset the form");
  await form.requestStore.setState({
    page: {
      id: "basic-card-page",
    },
    "basic-card-page": {
    },
  });

  ok(!form.querySelector("#cc-number:-moz-ui-invalid"), "cc-number field is not visibly invalid");

  form.remove();
});

add_task(async function test_noCardNetworkSelected() {
  let form = new BasicCardForm();
  await form.promiseReady;
  display.appendChild(form);
  await asyncElementRendered();

  info("have an existing card in storage, with no network id");
  let card1 = deepClone(PTU.BasicCards.JohnDoe);
  card1.guid = "9864798564";
  delete card1["cc-type"];

  await form.requestStore.setState({
    page: {
      id: "basic-card-page",
    },
    "basic-card-page": {
      guid: card1.guid,
    },
    savedBasicCards: {
      [card1.guid]: deepClone(card1),
    },
  });
  await asyncElementRendered();
  checkCCForm(form, card1);
  is(document.getElementById("cc-type").selectedIndex, 0, "Initial empty option is selected");

  form.remove();
  await form.requestStore.reset();
});

</script>

</body>
</html>