author | Matthew Noorenberghe <mozilla@noorenberghe.ca> |
Tue, 03 Apr 2018 14:06:21 -0400 | |
changeset 411562 | 70eb77d5bd24e93c06d4547b028cdf9d12c5cce7 |
parent 411561 | 6045035ad2edad1ed5a8c336883db01aa73bd463 |
child 411563 | 69964a79c66ac99adb528e1c8365b6cece01387c |
push id | 101686 |
push user | aciure@mozilla.com |
push date | Tue, 03 Apr 2018 21:59:31 +0000 |
treeherder | mozilla-inbound@8d846598d35d [default view] [failures only] |
perfherder | [talos] [build metrics] [platform microbench] (compared to previous push) |
reviewers | jaws |
bugs | 1428414 |
milestone | 61.0a1 |
first release with | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
last release without | nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
|
--- a/browser/extensions/formautofill/content/autofillEditForms.js +++ b/browser/extensions/formautofill/content/autofillEditForms.js @@ -24,17 +24,17 @@ class EditAutofillForm { } /** * Get inputs from the form. * @returns {object} */ buildFormObject() { return Array.from(this._elements.form.elements).reduce((obj, input) => { - if (input.value) { + if (input.value && !input.disabled) { obj[input.id] = input.value; } return obj; }, {}); } /** * Handle events
--- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -983,33 +983,33 @@ "is-fullwidth-code-point": "2.0.0" } }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=" }, - "string_decoder": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", - "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", - "requires": { - "safe-buffer": "5.1.1" - } - }, "string-width": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "requires": { "is-fullwidth-code-point": "2.0.0", "strip-ansi": "4.0.0" } }, + "string_decoder": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", + "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", + "requires": { + "safe-buffer": "5.1.1" + } + }, "strip-ansi": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", "requires": { "ansi-regex": "3.0.0" }, "dependencies": {
--- a/toolkit/components/payments/content/paymentDialogWrapper.js +++ b/toolkit/components/payments/content/paymentDialogWrapper.js @@ -458,16 +458,53 @@ var paymentDialogWrapper = { onChangeShippingOption({optionID}) { // Note, failing here on browser_host_name.js because the test closes // the dialog before the onChangeShippingOption is called, thus // deleting the request and making the requestId invalid. Unclear // why we aren't seeing the same issue with onChangeShippingAddress. paymentSrv.changeShippingOption(this.request.requestId, optionID); }, + async onUpdateAutofillRecord(collectionName, record, guid, { + errorStateChange, + preserveOldProperties, + selectedStateKey, + successStateChange, + }) { + if (collectionName == "creditCards" && !guid) { + // We need to be logged in so we can encrypt the credit card number and + // that's only supported when we're adding a new record. + // TODO: "MasterPassword.ensureLoggedIn" can be removed after the storage + // APIs are refactored to be async functions (bug 1399367). + if (!await MasterPassword.ensureLoggedIn()) { + Cu.reportError("User canceled master password entry"); + return; + } + } + + try { + if (guid) { + await formAutofillStorage[collectionName].update(guid, record, preserveOldProperties); + } else { + guid = await formAutofillStorage[collectionName].add(record); + } + + // Select the new record + if (selectedStateKey) { + Object.assign(successStateChange, { + [selectedStateKey]: guid, + }); + } + + this.sendMessageToContent("updateState", successStateChange); + } catch (ex) { + this.sendMessageToContent("updateState", errorStateChange); + } + }, + /** * @implements {nsIObserver} * @param {nsISupports} subject * @param {string} topic * @param {string} data */ observe(subject, topic, data) { switch (topic) { @@ -504,16 +541,25 @@ var paymentDialogWrapper = { case "paymentCancel": { this.onPaymentCancel(); break; } case "pay": { this.onPay(data); break; } + case "updateAutofillRecord": { + this.onUpdateAutofillRecord(data.collectionName, data.record, data.guid, { + errorStateChange: data.errorStateChange, + preserveOldProperties: data.preserveOldProperties, + selectedStateKey: data.selectedStateKey, + successStateChange: data.successStateChange, + }); + break; + } } }, }; if ("document" in this) { // Running in a browser, not a unit test let frame = document.getElementById("paymentRequestFrame"); let requestId = (new URLSearchParams(window.location.search)).get("requestId");
--- a/toolkit/components/payments/res/containers/basic-card-form.js +++ b/toolkit/components/payments/res/containers/basic-card-form.js @@ -1,32 +1,38 @@ /* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* import-globals-from ../../../../../browser/extensions/formautofill/content/autofillEditForms.js*/ /* import-globals-from ../mixins/PaymentStateSubscriberMixin.js */ /* import-globals-from ../unprivileged-fallbacks.js */ +/* import-globals-from ../paymentRequest.js */ "use strict"; /** * <basic-card-form></basic-card-form> * * XXX: Bug 1446164 - This form isn't localized when used via this custom element * as it will be much easier to share the logic once we switch to Fluent. */ class BasicCardForm extends PaymentStateSubscriberMixin(HTMLElement) { constructor() { super(); + this.genericErrorText = document.createElement("div"); + this.backButton = document.createElement("button"); this.backButton.addEventListener("click", this); + this.saveButton = document.createElement("button"); + this.saveButton.addEventListener("click", this); + // The markup is shared with form autofill preferences. let url = "formautofill/editCreditCard.xhtml"; this.promiseReady = this._fetchMarkup(url).then(doc => { this.form = doc.getElementById("form"); return this.form; }); } @@ -49,57 +55,107 @@ class BasicCardForm extends PaymentState let record = {}; this.formHandler = new EditCreditCard({ form, }, record, { isCCNumber: PaymentDialogUtils.isCCNumber, }); + this.appendChild(this.genericErrorText); this.appendChild(this.backButton); + this.appendChild(this.saveButton); // Only call the connected super callback(s) once our markup is fully // connected, including the shared form fetched asynchronously. super.connectedCallback(); }); } render(state) { this.backButton.textContent = this.dataset.backButtonLabel; + this.saveButton.textContent = this.dataset.saveButtonLabel; let record = {}; let { - selectedPaymentCard, + page, savedBasicCards, } = state; - let editing = !!state.selectedPaymentCard; + this.genericErrorText.textContent = page.error; + + let editing = !!page.guid; this.form.querySelector("#cc-number").disabled = editing; // If a card is selected we want to edit it. if (editing) { - record = savedBasicCards[selectedPaymentCard]; + record = savedBasicCards[page.guid]; if (!record) { - throw new Error("Trying to edit a non-existing card: " + selectedPaymentCard); + throw new Error("Trying to edit a non-existing card: " + page.guid); } } this.formHandler.loadRecord(record); } handleEvent(event) { switch (event.type) { case "click": { this.onClick(event); break; } } } onClick(evt) { - this.requestStore.setState({ - page: { - id: "payment-summary", + switch (evt.target) { + case this.backButton: { + this.requestStore.setState({ + page: { + id: "payment-summary", + }, + }); + break; + } + case this.saveButton: { + this.saveRecord(); + break; + } + default: { + throw new Error("Unexpected click target"); + } + } + } + + saveRecord() { + let record = this.formHandler.buildFormObject(); + let { + page, + } = this.requestStore.getState(); + + for (let editableFieldName of ["cc-name", "cc-exp-month", "cc-exp-year"]) { + record[editableFieldName] = record[editableFieldName] || ""; + } + + // Only save the card number if we're saving a new record, otherwise we'd + // overwrite the unmasked card number with the masked one. + if (!page.guid) { + record["cc-number"] = record["cc-number"] || ""; + } + + paymentRequest.updateAutofillRecord("creditCards", record, page.guid, { + errorStateChange: { + page: { + id: "basic-card-page", + error: this.dataset.errorGenericSave, + }, + }, + preserveOldProperties: true, + selectedStateKey: "selectedPaymentCard", + successStateChange: { + page: { + id: "payment-summary", + }, }, }); } } customElements.define("basic-card-form", BasicCardForm);
--- a/toolkit/components/payments/res/containers/payment-method-picker.js +++ b/toolkit/components/payments/res/containers/payment-method-picker.js @@ -130,20 +130,23 @@ class PaymentMethodPicker extends Paymen let nextState = { page: { id: "basic-card-page", }, }; switch (target) { case this.addLink: { - nextState.selectedPaymentCard = null; + nextState.page.guid = null; break; } case this.editLink: { + let state = this.requestStore.getState(); + let selectedPaymentCardGUID = state[this.selectedStateKey]; + nextState.page.guid = selectedPaymentCardGUID; break; } default: { throw new Error("Unexpected onClick"); } } this.requestStore.setState(nextState);
--- a/toolkit/components/payments/res/paymentRequest.js +++ b/toolkit/components/payments/res/paymentRequest.js @@ -126,15 +126,40 @@ var paymentRequest = { changeShippingAddress(data) { this.sendMessageToChrome("changeShippingAddress", data); }, changeShippingOption(data) { this.sendMessageToChrome("changeShippingOption", data); }, + /** + * Add/update an autofill storage record. + * + * If the the `guid` argument is provided update the record; otherwise, add it. + * @param {string} collectionName The autofill collection that record belongs to. + * @param {object} record The autofill record to add/update + * @param {string} [guid] The guid of the autofill record to update + */ + updateAutofillRecord(collectionName, record, guid, { + errorStateChange, + preserveOldProperties, + selectedStateKey, + successStateChange, + }) { + this.sendMessageToChrome("updateAutofillRecord", { + collectionName, + guid, + record, + errorStateChange, + preserveOldProperties, + selectedStateKey, + successStateChange, + }); + }, + onPaymentRequestUnload() { // remove listeners that may be used multiple times here window.removeEventListener("paymentChromeToContent", this); }, }; paymentRequest.init();
--- a/toolkit/components/payments/res/paymentRequest.xhtml +++ b/toolkit/components/payments/res/paymentRequest.xhtml @@ -19,17 +19,19 @@ <!ENTITY cancelPaymentButton.label "Cancel"> <!ENTITY approvePaymentButton.label "Pay"> <!ENTITY processingPaymentButton.label "Processing"> <!ENTITY successPaymentButton.label "Done"> <!ENTITY failPaymentButton.label "Fail"> <!ENTITY unknownPaymentButton.label "Unknown"> <!ENTITY orderDetailsLabel "Order Details"> <!ENTITY orderTotalLabel "Total"> + <!ENTITY basicCardPage.error.genericSave "There was an error saving the payment card."> <!ENTITY basicCardPage.backButton.label "Back"> + <!ENTITY basicCardPage.saveButton.label "Save"> ]> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <title>&paymentSummaryTitle;</title> <!-- chrome: is needed for global.dtd --> <meta http-equiv="Content-Security-Policy" content="default-src 'self' chrome:"/> @@ -121,17 +123,19 @@ </section> <section id="order-details-overlay" hidden="hidden"> <h1>&orderDetailsLabel;</h1> <order-details></order-details> </section> <basic-card-form id="basic-card-page" class="page" + data-error-generic-save="&basicCardPage.error.genericSave;" data-back-button-label="&basicCardPage.backButton.label;" + data-save-button-label="&basicCardPage.saveButton.label;" hidden="hidden"></basic-card-form> </div> <div id="disabled-overlay" hidden="hidden"> <!-- overlay to prevent changes while waiting for a response from the merchant --> </div> </template>
--- a/toolkit/components/payments/test/PaymentTestUtils.jsm +++ b/toolkit/components/payments/test/PaymentTestUtils.jsm @@ -168,16 +168,27 @@ var PaymentTestUtils = { // Waive the xray to access the untrusted `securityCodeInput` property let picker = Cu.waiveXrays(content.document.querySelector("payment-method-picker")); // Unwaive to access the ChromeOnly `setUserInput` API. // setUserInput dispatches changes events. Cu.unwaiveXrays(picker.securityCodeInput).setUserInput(securityCode); }, }, + DialogContentUtils: { + waitForState: async (content, stateCheckFn, msg) => { + const { + ContentTaskUtils, + } = ChromeUtils.import("resource://testing-common/ContentTaskUtils.jsm", {}); + let {requestStore} = Cu.waiveXrays(content.document.querySelector("payment-dialog")); + await ContentTaskUtils.waitForCondition(() => stateCheckFn(requestStore.getState()), msg); + return requestStore.getState(); + }, + }, + /** * Common PaymentMethodData for testing */ MethodData: { basicCard: { supportedMethods: "basic-card", }, bobPay: {
--- a/toolkit/components/payments/test/browser/browser.ini +++ b/toolkit/components/payments/test/browser/browser.ini @@ -1,16 +1,17 @@ [DEFAULT] head = head.js prefs = dom.payments.request.enabled=true skip-if = !e10s # Bug 1365964 - Payment Request isn't implemented for non-e10s support-files = blank_page.html +[browser_card_edit.js] [browser_change_shipping.js] [browser_host_name.js] [browser_profile_storage.js] [browser_request_serialization.js] [browser_request_shipping.js] [browser_request_summary.js] uses-unsafe-cpows = true [browser_shippingaddresschange_error.js]
new file mode 100644 --- /dev/null +++ b/toolkit/components/payments/test/browser/browser_card_edit.js @@ -0,0 +1,117 @@ +"use strict"; + +add_task(async function test_add_link() { + const args = { + methodData: [PTU.MethodData.basicCard], + details: PTU.Details.total60USD, + }; + await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, async function check() { + let { + PaymentTestUtils: PTU, + } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {}); + + let addLink = content.document.querySelector("payment-method-picker a"); + is(addLink.textContent, "Add", "Add link text"); + + addLink.click(); + + let state = await PTU.DialogContentUtils.waitForState(content, (state) => { + return state.page.id == "basic-card-page" && !state.page.guid; + }, + "Check add page state"); + + let year = (new Date()).getFullYear(); + let card = { + "cc-number": "4111111111111111", + "cc-name": "J. Smith", + "cc-exp-month": 11, + "cc-exp-year": year, + }; + + info("filling fields"); + for (let [key, val] of Object.entries(card)) { + let field = content.document.getElementById(key); + field.value = val; + ok(!field.disabled, `Field #${key} shouldn't be disabled`); + } + + content.document.querySelector("basic-card-form button:last-of-type").click(); + + state = await PTU.DialogContentUtils.waitForState(content, (state) => { + return Object.keys(state.savedBasicCards).length == 1; + }, + "Check card was added"); + + let cardGUIDs = Object.keys(state.savedBasicCards); + is(cardGUIDs.length, 1, "Check there is one card"); + let savedCard = state.savedBasicCards[cardGUIDs[0]]; + card["cc-number"] = "************1111"; // Card should be masked + for (let [key, val] of Object.entries(card)) { + is(savedCard[key], val, "Check " + key); + } + + state = await PTU.DialogContentUtils.waitForState(content, (state) => { + return state.page.id == "payment-summary"; + }, + "Switched back to payment-summary"); + }, args); +}); + +add_task(async function test_edit_link() { + const args = { + methodData: [PTU.MethodData.basicCard], + details: PTU.Details.total60USD, + }; + await spawnInDialogForMerchantTask(PTU.ContentTasks.createRequest, async function check() { + let { + PaymentTestUtils: PTU, + } = ChromeUtils.import("resource://testing-common/PaymentTestUtils.jsm", {}); + + let editLink = content.document.querySelector("payment-method-picker a:nth-of-type(2)"); + is(editLink.textContent, "Edit", "Edit link text"); + + editLink.click(); + + let state = await PTU.DialogContentUtils.waitForState(content, (state) => { + return state.page.id == "basic-card-page" && !!state.page.guid; + }, + "Check edit page state"); + + let nextYear = (new Date()).getFullYear() + 1; + let card = { + // cc-number cannot be modified + "cc-name": "A. Nonymous", + "cc-exp-month": 3, + "cc-exp-year": nextYear, + }; + + info("overwriting field values"); + for (let [key, val] of Object.entries(card)) { + let field = content.document.getElementById(key); + field.value = val; + ok(!field.disabled, `Field #${key} shouldn't be disabled`); + } + ok(content.document.getElementById("cc-number").disabled, "cc-number field should be disabled"); + + content.document.querySelector("basic-card-form button:last-of-type").click(); + + state = await PTU.DialogContentUtils.waitForState(content, (state) => { + return Object.keys(state.savedBasicCards).length == 1; + }, + "Check card was added"); + + let cardGUIDs = Object.keys(state.savedBasicCards); + is(cardGUIDs.length, 1, "Check there is still one card"); + let savedCard = state.savedBasicCards[cardGUIDs[0]]; + is(savedCard["cc-number"], "************1111", "Card number should be masked and unmodified."); + for (let [key, val] of Object.entries(card)) { + is(savedCard[key], val, "Check updated " + key); + } + + state = await PTU.DialogContentUtils.waitForState(content, (state) => { + return state.page.id == "payment-summary"; + }, + "Switched back to payment-summary"); + }, args); +}); +
--- a/toolkit/components/payments/test/mochitest/mochitest.ini +++ b/toolkit/components/payments/test/mochitest/mochitest.ini @@ -1,15 +1,16 @@ [DEFAULT] prefs = dom.webcomponents.customelements.enabled=false support-files = ../../../../../browser/extensions/formautofill/content/autofillEditForms.js ../../../../../testing/modules/sinon-2.3.2.js ../../res/paymentRequest.css + ../../res/paymentRequest.js ../../res/paymentRequest.xhtml ../../res/PaymentsStore.js ../../res/unprivileged-fallbacks.js ../../res/components/currency-amount.js ../../res/components/address-option.js ../../res/components/address-option.css ../../res/components/basic-card-option.js ../../res/components/basic-card-option.css
--- a/toolkit/components/payments/test/mochitest/payments_common.js +++ b/toolkit/components/payments/test/mochitest/payments_common.js @@ -1,11 +1,12 @@ "use strict"; -/* exported asyncElementRendered, promiseStateChange, deepClone, PTU */ +/* exported asyncElementRendered, promiseStateChange, promiseContentToChromeMessage, deepClone, + PTU */ const PTU = SpecialPowers.Cu.import("resource://testing-common/PaymentTestUtils.jsm", {}) .PaymentTestUtils; /** * A helper to await on while waiting for an asynchronous rendering of a Custom * Element. * @returns {Promise} @@ -20,11 +21,28 @@ function promiseStateChange(store) { stateChangeCallback(state) { store.unsubscribe(this); resolve(state); }, }); }); } +/** + * Wait for a message of `messageType` from content to chrome and resolve with the event details. + * @param {string} messageType of the expected message + * @returns {Promise} when the message is dispatched + */ +function promiseContentToChromeMessage(messageType) { + return new Promise(resolve => { + document.addEventListener("paymentContentToChrome", function onCToC(event) { + if (event.detail.messageType != messageType) { + return; + } + document.removeEventListener("paymentContentToChrome", onCToC); + resolve(event.detail); + }); + }); +} + function deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }
--- a/toolkit/components/payments/test/mochitest/test_basic_card_form.html +++ b/toolkit/components/payments/test/mochitest/test_basic_card_form.html @@ -12,16 +12,17 @@ Test the basic-card-form element <script src="sinon-2.3.2.js"></script> <script src="payments_common.js"></script> <script src="custom-elements.min.js"></script> <script src="unprivileged-fallbacks.js"></script> <script src="PaymentsStore.js"></script> <script src="PaymentStateSubscriberMixin.js"></script> <script src="autofillEditForms.js"></script> <script src="basic-card-form.js"></script> + <script src="paymentRequest.js"></script> <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/> <link rel="stylesheet" type="text/css" href="paymentRequest.css"/> </head> <body> <p id="display"> </p> <div id="content" style="display: none"> @@ -80,29 +81,100 @@ add_task(async function test_backButton( 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 = document.createElement("basic-card-form"); + form.dataset.saveButtonLabel = "Save"; + form.dataset.errorGenericSave = "Generic error"; + await form.promiseReady; + display.appendChild(form); + await asyncElementRendered(); + + form.form.querySelector("#cc-number").focus(); + sendString("4111111111111111"); + form.form.querySelector("#cc-name").focus(); + sendString("J. Smith"); + form.form.querySelector("#cc-exp-month").focus(); + sendString("11"); + form.form.querySelector("#cc-exp-year").focus(); + let year = (new Date()).getFullYear().toString(); + sendString(year); + + let messagePromise = promiseContentToChromeMessage("updateAutofillRecord"); + is(form.saveButton.textContent, "Save", "Check label"); + synthesizeMouseAtCenter(form.saveButton, {}); + + let details = await messagePromise; + is(details.collectionName, "creditCards", "Check collectionName"); + isDeeply(details, { + collectionName: "creditCards", + errorStateChange: { + page: { + id: "basic-card-page", + error: "Generic error", + }, + }, + guid: undefined, + messageType: "updateAutofillRecord", + preserveOldProperties: true, + record: { + "cc-exp-month": "11", + "cc-exp-year": year, + "cc-name": "J. Smith", + "cc-number": "4111111111111111", + }, + selectedStateKey: "selectedPaymentCard", + successStateChange: { + page: { + id: "payment-summary", + }, + }, + }, "Check event details for the message to chrome"); + form.remove(); +}); + +add_task(async function test_genericError() { + let form = document.createElement("basic-card-form"); + 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_record() { let form = document.createElement("basic-card-form"); await form.promiseReady; display.appendChild(form); await asyncElementRendered(); info("test year before current"); let card1 = deepClone(PTU.BasicCards.JohnDoe); card1.guid = "9864798564"; card1["cc-exp-year"] = 2011; await form.requestStore.setState({ - selectedPaymentCard: card1.guid, + page: { + id: "basic-card-page", + guid: card1.guid, + }, savedBasicCards: { [card1.guid]: deepClone(card1), }, }); await asyncElementRendered(); checkCCForm(form, card1); info("test future year"); @@ -118,27 +190,32 @@ add_task(async function test_record() { info("test change to minimal record"); let minimalCard = { // no expiration date or name "cc-number": "1234567690123", guid: "9gnjdhen46", }; await form.requestStore.setState({ - selectedPaymentCard: minimalCard.guid, + page: { + id: "basic-card-page", + guid: minimalCard.guid, + }, savedBasicCards: { [minimalCard.guid]: deepClone(minimalCard), }, }); await asyncElementRendered(); checkCCForm(form, minimalCard); info("change to no selected card"); await form.requestStore.setState({ - selectedPaymentCard: null, + page: { + id: "basic-card-page", + }, }); await asyncElementRendered(); checkCCForm(form, {}); form.remove(); }); </script>