diff options
Diffstat (limited to 'browser/extensions/formautofill/test/browser/creditCard')
19 files changed, 3590 insertions, 0 deletions
diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser.ini b/browser/extensions/formautofill/test/browser/creditCard/browser.ini new file mode 100644 index 0000000000..613b3cf126 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser.ini @@ -0,0 +1,51 @@ +[DEFAULT] +prefs = + extensions.formautofill.creditCards.enabled=true + extensions.formautofill.reauth.enabled=true + # lower the interval for event telemetry in the content process to update the parent process + toolkit.telemetry.ipcBatchTimeout=0 +support-files = + ../head.js + !/browser/extensions/formautofill/test/fixtures/autocomplete_basic.html + ../../fixtures/autocomplete_creditcard_basic.html + ../../fixtures/autocomplete_creditcard_iframe.html + ../../fixtures/autocomplete_creditcard_cc_exp_field.html + ../../fixtures/without_autocomplete_creditcard_basic.html + head_cc.js + +[browser_anti_clickjacking.js] +skip-if = !debug && os == "mac" # perma-fail see Bug 1600059 +[browser_creditCard_doorhanger_action.js] +skip-if = (!debug && os == "mac") || (os == "win" && ccov) # perma-fail see Bug 1655601, Bug 1655600 +[browser_creditCard_doorhanger_display.js] +skip-if = (!debug && os == "mac") || (os == "win" && ccov) # perma-fail see Bug 1655601, Bug 1655600 +[browser_creditCard_doorhanger_fields.js] +skip-if = (!debug && os == "mac") || (os == "win" && ccov) # perma-fail see Bug 1655601, Bug 1655600 +[browser_creditCard_doorhanger_iframe.js] +skip-if = (!debug && os == "mac") || (os == "win" && ccov) # perma-fail see Bug 1655601, Bug 1655600 +[browser_creditCard_doorhanger_logo.js] +skip-if = (!debug && os == "mac") || (os == "win" && ccov) # perma-fail see Bug 1655601, Bug 1655600 +[browser_creditCard_doorhanger_sync.js] +skip-if = (!debug && os == "mac") || (os == "win" && ccov) # perma-fail see Bug 1655601, Bug 1655600 +[browser_creditCard_dropdown_layout.js] +skip-if = ((os == "mac") || (os == 'linux') || (os == 'win')) +[browser_creditCard_fill_cancel_login.js] +skip-if = ((!debug && os == "mac") || (os == 'linux') || (os == 'win')) +[browser_creditCard_heuristics.js] +skip-if = apple_silicon && !debug # Bug 1714221 +[browser_creditCard_heuristics_cc_type.js] +skip-if = apple_silicon && !debug # Bug 1714221 +[browser_creditCard_submission_autodetect_type.js] +skip-if = apple_silicon && !debug +[browser_creditCard_submission_normalized.js] +skip-if = apple_silicon && !debug +[browser_creditCard_telemetry.js] +skip-if = + apple_silicon && !debug # Bug 1714221 + os == "win" && os_version == "6.1" # Skip on Azure - frequent failure +[browser_editCreditCardDialog.js] +skip-if = ((os == 'linux') || (os == "mac") || (os == 'win')) # perma-fail see Bug 1600059 +[browser_insecure_form.js] +skip-if = ((os == 'mac') || (os == 'linux') || (os == 'win')) # bug 1456284 +[browser_manageCreditCardsDialog.js] +skip-if = ((os == 'win') || (os == 'mac') || (os == 'linux')) diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js b/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js new file mode 100644 index 0000000000..ff5feb54df --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_anti_clickjacking.js @@ -0,0 +1,123 @@ +"use strict"; + +const ADDRESS_URL = + "http://example.org/browser/browser/extensions/formautofill/test/browser/autocomplete_basic.html"; +const CC_URL = + "https://example.org/browser/browser/extensions/formautofill/test/browser/creditCard/autocomplete_creditcard_basic.html"; + +add_task(async function setup_storage() { + await setStorage( + TEST_ADDRESS_1, + TEST_ADDRESS_2, + TEST_ADDRESS_3, + TEST_CREDIT_CARD_1, + TEST_CREDIT_CARD_2, + TEST_CREDIT_CARD_3 + ); +}); + +add_task(async function test_active_delay() { + // This is a workaround for the fact that we don't have a way + // to know when the popup was opened exactly and this makes our test + // racy when ensuring that we first test for disabled items before + // the delayed enabling happens. + // + // In the future we should consider adding an event when a popup + // gets opened and listen for it in this test before we check if the item + // is disabled. + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.notification_enable_delay", 1000], + ["extensions.formautofill.reauth.enabled", false], + ], + }); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CC_URL }, + async function (browser) { + const focusInput = "#cc-number"; + + // Open the popup -- we don't use openPopupOn() because there + // are things we need to check between these steps. + await SimpleTest.promiseFocus(browser); + const start = performance.now(); + await runAndWaitForAutocompletePopupOpen(browser, async () => { + await focusAndWaitForFieldsIdentified(browser, focusInput); + }); + const firstItem = getDisplayedPopupItems(browser)[0]; + ok(firstItem.disabled, "Popup should be disbled upon opening."); + is( + browser.autoCompletePopup.selectedIndex, + -1, + "No item selected at first" + ); + + // Check that clicking on menu doesn't do anything while + // it is disabled + firstItem.click(); + is( + browser.autoCompletePopup.selectedIndex, + -1, + "No item selected after clicking on disabled item" + ); + + // Check that the delay before enabling is as long as expected + await waitForPopupEnabled(browser); + const delta = performance.now() - start; + info(`Popup was disabled for ${delta} ms`); + ok(delta >= 1000, "Popup was disabled for at least 1000 ms"); + + // Check the clicking on the menu works now + firstItem.click(); + is( + browser.autoCompletePopup.selectedIndex, + 0, + "First item selected after clicking on enabled item" + ); + + // Clean up + await closePopup(browser); + } + ); +}); + +add_task(async function test_no_delay() { + await SpecialPowers.pushPrefEnv({ + set: [ + ["security.notification_enable_delay", 1000], + ["extensions.formautofill.reauth.enabled", false], + ], + }); + await BrowserTestUtils.withNewTab( + { gBrowser, url: ADDRESS_URL }, + async function (browser) { + const focusInput = "#organization"; + + // Open the popup -- we don't use openPopupOn() because there + // are things we need to check between these steps. + await SimpleTest.promiseFocus(browser); + await runAndWaitForAutocompletePopupOpen(browser, async () => { + await focusAndWaitForFieldsIdentified(browser, focusInput); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + }); + const firstItem = getDisplayedPopupItems(browser)[0]; + ok(!firstItem.disabled, "Popup should be enabled upon opening."); + is( + browser.autoCompletePopup.selectedIndex, + -1, + "No item selected at first" + ); + + // Check that clicking on menu doesn't do anything while + // it is disabled + firstItem.click(); + is( + browser.autoCompletePopup.selectedIndex, + 0, + "First item selected after clicking on enabled item" + ); + + // Clean up + await closePopup(browser); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js new file mode 100644 index 0000000000..82122925d7 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_action.js @@ -0,0 +1,170 @@ +"use strict"; + +add_task(async function test_save_doorhanger_click_save() { + let onChanged = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + "#cc-number": "5577000055770004", + "#cc-exp-month": "12", + "#cc-exp-year": "2017", + "#cc-type": "mastercard", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + is(creditCards[0]["cc-name"], "User 1", "Verify the name field"); + is(creditCards[0]["cc-type"], "mastercard", "Verify the cc-type field"); + await removeAllRecords(); +}); + +add_task(async function test_save_doorhanger_click_never_save() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 0", + "#cc-number": "6387060366272981", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MENU_BUTTON, 0); + } + ); + + await sleep(1000); + let creditCards = await getCreditCards(); + let creditCardPref = SpecialPowers.getBoolPref( + ENABLED_AUTOFILL_CREDITCARDS_PREF + ); + is(creditCards.length, 0, "No credit card in storage"); + is(creditCardPref, false, "Credit card is disabled"); + SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF); +}); + +add_task(async function test_save_doorhanger_click_cancel_save() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + "#cc-number": "5038146897157463", + }, + }); + + ok( + !SpecialPowers.Services.prefs.prefHasUserValue(SYNC_USERNAME_PREF), + "Sync account should not exist by default" + ); + await onPopupShown; + let cb = getDoorhangerCheckbox(); + ok(cb.hidden, "Sync checkbox should be hidden"); + await clickDoorhangerButton(SECONDARY_BUTTON); + } + ); + + await sleep(1000); + let creditCards = await getCreditCards(); + is(creditCards.length, 0, "No credit card saved"); +}); + +add_task(async function test_update_doorhanger_click_update() { + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + let onChanged = waitForStorageChangedEvents("update", "notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "Mark Smith", + "#cc-number": "4111111111111111", + "#cc-exp-month": "4", + "#cc-exp-year": new Date().getFullYear(), + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + + creditCards = await getCreditCards(); + is(creditCards.length, 1, "Still 1 credit card in storage"); + is(creditCards[0]["cc-name"], "Mark Smith", "name field got updated"); + await removeAllRecords(); +}); + +add_task(async function test_update_doorhanger_click_save() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + let onChanged = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let osKeyStoreLoginShown = + OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + let onPopupShown = waitForPopupShown(); + await openPopupOn(browser, "form #cc-name"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill(browser, "#cc-name", "John Doe"); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(SECONDARY_BUTTON); + await osKeyStoreLoginShown; + } + ); + await onChanged; + + creditCards = await getCreditCards(); + is(creditCards.length, 2, "2 credit cards in storage"); + is( + creditCards[0]["cc-name"], + TEST_CREDIT_CARD_1["cc-name"], + "Original record's cc-name field is unchanged" + ); + is(creditCards[1]["cc-name"], "User 1", "cc-name field in the new record"); + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js new file mode 100644 index 0000000000..715eceb3eb --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_display.js @@ -0,0 +1,311 @@ +"use strict"; + +add_task(async function test_save_doorhanger_shown_no_profile() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + "#cc-number": "5577000055770004", + "#cc-exp-month": "12", + "#cc-exp-year": "2017", + "#cc-type": "mastercard", + }, + }); + + await onPopupShown; + } + ); +}); + +add_task(async function test_save_doorhanger_shown_different_card_number() { + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-number": TEST_CREDIT_CARD_2["cc-number"], + }, + }); + + await onPopupShown; + } + ); + await removeAllRecords(); +}); + +add_task(async function test_update_doorhanger_shown_different_card_name() { + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "Mark Smith", + "#cc-number": TEST_CREDIT_CARD_1["cc-number"], + "#cc-exp-month": TEST_CREDIT_CARD_1["cc-exp-month"], + "#cc-exp-year": TEST_CREDIT_CARD_1["cc-exp-year"], + }, + }); + + await onPopupShown; + } + ); + await removeAllRecords(); +}); + +add_task(async function test_update_doorhanger_shown_different_card_expiry() { + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": TEST_CREDIT_CARD_1["cc-name"], + "#cc-number": TEST_CREDIT_CARD_1["cc-number"], + "#cc-exp-month": "12", + "#cc-exp-year": "2099", + }, + }); + + await onPopupShown; + } + ); + await removeAllRecords(); +}); + +add_task(async function test_doorhanger_not_shown_when_autofill_untouched() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let osKeyStoreLoginShown = + OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + await openPopupOn(browser, "form #cc-name"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await osKeyStoreLoginShown; + await waitForAutofill(browser, "#cc-name", "John Doe"); + + await SpecialPowers.spawn(browser, [], async function () { + let form = content.document.getElementById("form"); + form.querySelector("input[type=submit]").click(); + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onUsed; + + creditCards = await getCreditCards(); + is(creditCards.length, 1, "Still 1 credit card"); + is(creditCards[0].timesUsed, 1, "timesUsed field set to 1"); + await removeAllRecords(); +}); + +add_task(async function test_doorhanger_not_shown_when_fill_duplicate() { + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "John Doe", + "#cc-number": "4111111111111111", + "#cc-exp-month": "4", + "#cc-exp-year": new Date().getFullYear(), + "#cc-type": "visa", + }, + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onUsed; + + creditCards = await getCreditCards(); + is(creditCards.length, 1, "Still 1 credit card in storage"); + is( + creditCards[0]["cc-name"], + TEST_CREDIT_CARD_1["cc-name"], + "Verify the name field" + ); + is(creditCards[0].timesUsed, 1, "timesUsed field set to 1"); + await removeAllRecords(); +}); + +add_task( + async function test_doorhanger_not_shown_when_autofill_then_fill_everything_duplicate() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2); + let creditCards = await getCreditCards(); + is(creditCards.length, 2, "2 credit card in storage"); + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let osKeyStoreLoginShown = + OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + await openPopupOn(browser, "form #cc-number"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + "#cc-number", + TEST_CREDIT_CARD_1["cc-number"] + ); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + // Change number to the second credit card number + "#cc-number": TEST_CREDIT_CARD_2["cc-number"], + "#cc-name": TEST_CREDIT_CARD_2["cc-name"], + "#cc-exp-month": TEST_CREDIT_CARD_2["cc-exp-month"], + "#cc-exp-year": TEST_CREDIT_CARD_2["cc-exp-year"], + }, + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + await osKeyStoreLoginShown; + } + ); + await onUsed; + + creditCards = await getCreditCards(); + is(creditCards.length, 2, "Still 2 credit card"); + await removeAllRecords(); + } +); + +add_task( + async function test_doorhanger_not_shown_when_autofill_then_fill_number_duplicate() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_1, { + ...TEST_CREDIT_CARD_1, + ...{ "cc-number": "5105105105105100" }, + }); + + let creditCards = await getCreditCards(); + is(creditCards.length, 2, "2 credit card in storage"); + let onUsed = waitForStorageChangedEvents("notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let osKeyStoreLoginShown = + OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + await openPopupOn(browser, "form #cc-number"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await waitForAutofill( + browser, + "#cc-number", + TEST_CREDIT_CARD_1["cc-number"] + ); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + // Change number to the second credit card number + "#cc-number": "5105105105105100", + }, + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + await osKeyStoreLoginShown; + } + ); + await onUsed; + + creditCards = await getCreditCards(); + is(creditCards.length, 2, "Still 2 credit card"); + await removeAllRecords(); + } +); + +add_task(async function test_update_doorhanger_shown_when_fill_mergeable() { + await setStorage(TEST_CREDIT_CARD_3); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + let onChanged = waitForStorageChangedEvents("update", "notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 3", + "#cc-number": "5103059495477870", + "#cc-exp-month": "1", + "#cc-exp-year": "2000", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + + creditCards = await getCreditCards(); + is(creditCards.length, 1, "Still 1 credit card in storage"); + is(creditCards[0]["cc-name"], "User 3", "Verify the name field"); + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js new file mode 100644 index 0000000000..c1ebef737e --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_fields.js @@ -0,0 +1,198 @@ +"use strict"; + +add_task(async function test_update_autofill_name_field() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + let onChanged = waitForStorageChangedEvents("update", "notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let osKeyStoreLoginShown = + OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + let onPopupShown = waitForPopupShown(); + + await openPopupOn(browser, "form #cc-name"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await osKeyStoreLoginShown; + await waitForAutofill(browser, "#cc-name", "John Doe"); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + + creditCards = await getCreditCards(); + is(creditCards.length, 1, "Still 1 credit card"); + is(creditCards[0]["cc-name"], "User 1", "cc-name field is updated"); + is( + creditCards[0]["cc-number"], + "************1111", + "Verify the card number field" + ); + await removeAllRecords(); +}); + +add_task(async function test_update_autofill_exp_date_field() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + let onChanged = waitForStorageChangedEvents("update", "notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let osKeyStoreLoginShown = + OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + let onPopupShown = waitForPopupShown(); + await openPopupOn(browser, "form #cc-name"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await osKeyStoreLoginShown; + await waitForAutofill(browser, "#cc-name", "John Doe"); + + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-exp-year": "2019", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + + creditCards = await getCreditCards(); + is(creditCards.length, 1, "Still 1 credit card"); + is(creditCards[0]["cc-exp-year"], 2019, "cc-exp-year field is updated"); + is( + creditCards[0]["cc-number"], + "************1111", + "Verify the card number field" + ); + await removeAllRecords(); +}); + +add_task(async function test_submit_unnormailzed_field() { + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "John Doe", + "#cc-number": "4111111111111111", + "#cc-exp-month": "4", + "#cc-exp-year": new Date().getFullYear().toString().substr(2, 2), + "#cc-type": "visa", + }, + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + + creditCards = await getCreditCards(); + is(creditCards.length, 1, "Still 1 credit card in storage"); + is( + creditCards[0]["cc-exp-year"], + new Date().getFullYear(), + "Verify the expiry year field" + ); + await removeAllRecords(); +}); + +add_task(async function test_submit_invalid_network_field() { + let onChanged = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + "#cc-number": "5038146897157463", + "#cc-exp-month": "12", + "#cc-exp-year": "2017", + "#cc-type": "gringotts", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + is(creditCards[0]["cc-name"], "User 1", "Verify the name field"); + is( + creditCards[0]["cc-type"], + undefined, + "Invalid network/cc-type was not saved" + ); + + await removeAllRecords(); +}); + +add_task(async function test_submit_combined_expiry_field() { + let onChanged = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_COMBINED_EXPIRY_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "John Doe", + "#cc-number": "374542158116607", + "#cc-exp": "05/28", + }, + }); + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "Card should be added"); + is(creditCards[0]["cc-exp"], "2028-05", "Verify cc-exp field"); + is(creditCards[0]["cc-exp-month"], 5, "Verify cc-exp-month field"); + is(creditCards[0]["cc-exp-year"], 2028, "Verify cc-exp-year field"); + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js new file mode 100644 index 0000000000..2781e5acf6 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_iframe.js @@ -0,0 +1,103 @@ +"use strict"; + +add_task(async function test_iframe_submit_untouched_creditCard_form() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + // This test triggers two form submission events so cc 'timesUsed' count is 2. + // The first submission is triggered by standard form submission, and the + // second is triggered by page hiding. + const EXPECTED_ON_USED_COUNT = 2; + let notifyUsedCounter = EXPECTED_ON_USED_COUNT; + let onUsed = TestUtils.topicObserved( + "formautofill-storage-changed", + (subject, data) => { + if (data == "notifyUsed") { + notifyUsedCounter--; + } + return notifyUsedCounter == 0; + } + ); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_IFRAME_URL }, + async function (browser) { + let osKeyStoreLoginShown = + OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + let iframeBC = browser.browsingContext.children[0]; + await openPopupOnSubframe(browser, iframeBC, "form #cc-name"); + + EventUtils.synthesizeKey("VK_DOWN", {}); + EventUtils.synthesizeKey("VK_RETURN", {}); + await osKeyStoreLoginShown; + await waitForAutofill(iframeBC, "#cc-name", "John Doe"); + + await SpecialPowers.spawn(iframeBC, [], async function () { + let form = content.document.getElementById("form"); + form.querySelector("input[type=submit]").click(); + }); + + await sleep(1000); + is(PopupNotifications.panel.state, "closed", "Doorhanger is hidden"); + } + ); + await onUsed; + + creditCards = await getCreditCards(); + is(creditCards.length, 1, "Still 1 credit card"); + is( + creditCards[0].timesUsed, + EXPECTED_ON_USED_COUNT, + "timesUsed field set to 2" + ); + await removeAllRecords(); +}); + +add_task(async function test_iframe_unload_save_card() { + let onChanged = waitForStorageChangedEvents("add"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_IFRAME_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let iframeBC = browser.browsingContext.children[0]; + await focusUpdateSubmitForm( + iframeBC, + { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + "#cc-number": "4556194630960970", + "#cc-exp-month": "10", + "#cc-exp-year": "2024", + "#cc-type": "visa", + }, + }, + false + ); + + info("Removing iframe without submitting"); + await SpecialPowers.spawn(browser, [], async function () { + let frame = content.document.querySelector("iframe"); + frame.remove(); + }); + + await onPopupShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + is(creditCards[0]["cc-name"], "User 1", "Verify the name field"); + is(creditCards[0]["cc-type"], "visa", "Verify the cc-type field"); + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js new file mode 100644 index 0000000000..9c4f4e4825 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_logo.js @@ -0,0 +1,238 @@ +"use strict"; + +/* + The next four tests look very similar because if we try to do multiple + credit card operations in one test, there's a good chance the test will timeout + and produce an invalid result. + We mitigate this issue by having each test only deal with one credit card in storage + and one credit card operation. +*/ +add_task(async function test_submit_third_party_creditCard_logo() { + const amexCard = { + "cc-number": "374542158116607", + "cc-type": "amex", + "cc-name": "John Doe", + }; + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + "#cc-number": amexCard["cc-number"], + }, + }); + + await onPopupShown; + let doorhanger = getNotification(); + let creditCardLogo = doorhanger.querySelector(".desc-message-box image"); + let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0]; + + is( + creditCardLogoWithoutExtension, + "chrome://formautofill/content/third-party/cc-logo-amex", + "CC logo should be amex" + ); + + await clickDoorhangerButton(SECONDARY_BUTTON); + } + ); + await removeAllRecords(); +}); + +add_task(async function test_update_third_party_creditCard_logo() { + const amexCard = { + "cc-number": "374542158116607", + "cc-name": "John Doe", + }; + + await setStorage(amexCard); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + let onChanged = waitForStorageChangedEvents("update"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "Mark Smith", + "#cc-number": amexCard["cc-number"], + "#cc-exp-month": "4", + "#cc-exp-year": new Date().getFullYear(), + }, + }); + + await onPopupShown; + + let doorhanger = getNotification(); + let creditCardLogo = doorhanger.querySelector(".desc-message-box image"); + let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0]; + is( + creditCardLogoWithoutExtension, + `chrome://formautofill/content/third-party/cc-logo-amex`, + `CC Logo should be amex` + ); + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + await removeAllRecords(); +}); + +add_task(async function test_submit_generic_creditCard_logo() { + const genericCard = { + "cc-number": "937899583135", + "cc-name": "John Doe", + }; + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + "#cc-number": genericCard["cc-number"], + }, + }); + + await onPopupShown; + let doorhanger = getNotification(); + let creditCardLogo = doorhanger.querySelector(".desc-message-box image"); + let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0]; + + is( + creditCardLogoWithoutExtension, + "chrome://formautofill/content/icon-credit-card-generic", + "CC logo should be generic" + ); + + await clickDoorhangerButton(SECONDARY_BUTTON); + } + ); + await removeAllRecords(); +}); + +add_task(async function test_update_generic_creditCard_logo() { + const genericCard = { + "cc-number": "937899583135", + "cc-name": "John Doe", + }; + + await setStorage(genericCard); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + let onChanged = waitForStorageChangedEvents("update"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "Mark Smith", + "#cc-number": genericCard["cc-number"], + "#cc-exp-month": "4", + "#cc-exp-year": new Date().getFullYear(), + }, + }); + + await onPopupShown; + + let doorhanger = getNotification(); + let creditCardLogo = doorhanger.querySelector(".desc-message-box image"); + let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0]; + is( + creditCardLogoWithoutExtension, + `chrome://formautofill/content/icon-credit-card-generic`, + `CC Logo should be generic` + ); + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + await removeAllRecords(); +}); + +add_task(async function test_save_panel_spaces_in_cc_number_logo() { + const amexCard = { + "cc-number": "37 4542 158116 607", + "cc-type": "amex", + "cc-name": "John Doe", + }; + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-number": amexCard["cc-number"], + }, + }); + + await onPopupShown; + let doorhanger = getNotification(); + let creditCardLogo = doorhanger.querySelector(".desc-message-box image"); + let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0]; + + is( + creditCardLogoWithoutExtension, + "chrome://formautofill/content/third-party/cc-logo-amex", + "CC logo should be amex" + ); + + await clickDoorhangerButton(SECONDARY_BUTTON); + } + ); +}); + +add_task(async function test_update_panel_with_spaces_in_cc_number_logo() { + const amexCard = { + "cc-number": "374 54215 8116607", + "cc-name": "John Doe", + }; + + await setStorage(amexCard); + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "1 credit card in storage"); + + let onChanged = waitForStorageChangedEvents("update", "notifyUsed"); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "Mark Smith", + "#cc-number": amexCard["cc-number"], + "#cc-exp-month": "4", + "#cc-exp-year": new Date().getFullYear(), + }, + }); + + await onPopupShown; + + let doorhanger = getNotification(); + let creditCardLogo = doorhanger.querySelector(".desc-message-box image"); + let creditCardLogoWithoutExtension = creditCardLogo.src.split(".", 1)[0]; + is( + creditCardLogoWithoutExtension, + `chrome://formautofill/content/third-party/cc-logo-amex`, + `CC Logo should be amex` + ); + // We are not testing whether the cc-number is saved correctly, + // we only care that the logo in the update panel shows correctly. + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + await onChanged; + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js new file mode 100644 index 0000000000..2064928820 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_doorhanger_sync.js @@ -0,0 +1,117 @@ +"use strict"; + +add_task(async function test_submit_creditCard_with_sync_account() { + await SpecialPowers.pushPrefEnv({ + set: [ + [SYNC_USERNAME_PREF, "foo@bar.com"], + [SYNC_CREDITCARDS_AVAILABLE_PREF, true], + [ENABLED_AUTOFILL_CREDITCARDS_PREF, true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 2", + "#cc-number": "6387060366272981", + }, + }); + + await onPopupShown; + let cb = getDoorhangerCheckbox(); + ok(!cb.hidden, "Sync checkbox should be visible"); + is( + SpecialPowers.getBoolPref(SYNC_CREDITCARDS_PREF), + false, + "creditCards sync should be disabled by default" + ); + + // Verify if the checkbox and button state is changed. + let secondaryButton = getDoorhangerButton(SECONDARY_BUTTON); + let menuButton = getDoorhangerButton(MENU_BUTTON); + is( + cb.checked, + false, + "Checkbox state should match creditCards sync state" + ); + is( + secondaryButton.disabled, + false, + "Not saving button should be enabled" + ); + is( + menuButton.disabled, + false, + "Never saving menu button should be enabled" + ); + // Click the checkbox to enable credit card sync. + cb.click(); + is( + SpecialPowers.getBoolPref(SYNC_CREDITCARDS_PREF), + true, + "creditCards sync should be enabled after checked" + ); + is( + secondaryButton.disabled, + true, + "Not saving button should be disabled" + ); + is( + menuButton.disabled, + true, + "Never saving menu button should be disabled" + ); + // Click the checkbox again to disable credit card sync. + cb.click(); + is( + SpecialPowers.getBoolPref(SYNC_CREDITCARDS_PREF), + false, + "creditCards sync should be disabled after unchecked" + ); + is( + secondaryButton.disabled, + false, + "Not saving button should be enabled again" + ); + is( + menuButton.disabled, + false, + "Never saving menu button should be enabled again" + ); + await clickDoorhangerButton(SECONDARY_BUTTON); + } + ); +}); + +add_task(async function test_submit_creditCard_with_synced_already() { + await SpecialPowers.pushPrefEnv({ + set: [ + [SYNC_CREDITCARDS_PREF, true], + [SYNC_USERNAME_PREF, "foo@bar.com"], + [SYNC_CREDITCARDS_AVAILABLE_PREF, true], + ], + }); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 2", + "#cc-number": "6387060366272981", + }, + }); + + await onPopupShown; + let cb = getDoorhangerCheckbox(); + ok(cb.hidden, "Sync checkbox should be hidden"); + await clickDoorhangerButton(SECONDARY_BUTTON); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js new file mode 100644 index 0000000000..d69f7129ee --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_dropdown_layout.js @@ -0,0 +1,57 @@ +"use strict"; + +const CC_URL = + "https://example.org/browser/browser/extensions/formautofill/test/browser/creditCard/autocomplete_creditcard_basic.html"; + +add_task(async function setup_storage() { + await setStorage(TEST_CREDIT_CARD_1, TEST_CREDIT_CARD_2, TEST_CREDIT_CARD_3); +}); + +async function reopenPopupWithResizedInput(browser, selector, newSize) { + await closePopup(browser); + /* eslint no-shadow: ["error", { "allow": ["selector", "newSize"] }] */ + await SpecialPowers.spawn( + browser, + [{ selector, newSize }], + async function ({ selector, newSize }) { + const input = content.document.querySelector(selector); + + input.style.boxSizing = "border-box"; + input.style.width = newSize + "px"; + } + ); + await openPopupOn(browser, selector); +} + +add_task(async function test_credit_card_dropdown() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: CC_URL }, + async function (browser) { + const focusInput = "#cc-number"; + await openPopupOn(browser, focusInput); + const firstItem = getDisplayedPopupItems(browser)[0]; + + isnot(firstItem.getAttribute("ac-image"), "", "Should show icon"); + ok( + firstItem.getAttribute("aria-label").startsWith("Visa "), + "aria-label should start with Visa" + ); + + // The breakpoint of two-lines layout is 185px + await reopenPopupWithResizedInput(browser, focusInput, 175); + is( + firstItem._itemBox.getAttribute("size"), + "small", + "Show two-lines layout" + ); + await reopenPopupWithResizedInput(browser, focusInput, 195); + is( + firstItem._itemBox.hasAttribute("size"), + false, + "Show one-line layout" + ); + + await closePopup(browser); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js new file mode 100644 index 0000000000..6304e4699f --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_fill_cancel_login.js @@ -0,0 +1,37 @@ +"use strict"; + +add_task(async function test_fill_creditCard_but_cancel_login() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_2); + + let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(false); // cancel + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + await openPopupOn(browser, "#cc-name"); + const ccItem = getDisplayedPopupItems(browser)[0]; + let popupClosePromise = BrowserTestUtils.waitForPopupEvent( + browser.autoCompletePopup, + "hidden" + ); + await EventUtils.synthesizeMouseAtCenter(ccItem, {}); + await Promise.all([osKeyStoreLoginShown, popupClosePromise]); + + await SpecialPowers.spawn(browser, [], async function () { + is(content.document.querySelector("#cc-name").value, "", "Check name"); + is( + content.document.querySelector("#cc-number").value, + "", + "Check number" + ); + }); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js new file mode 100644 index 0000000000..3801239234 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics.js @@ -0,0 +1,165 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TESTCASES = [ + { + description: "@autocomplete - all fields in the same form", + document: `<form> + <input id="cc-number" autocomplete="cc-number"> + <input id="cc-name" autocomplete="cc-name"> + <input id="cc-exp" autocomplete="cc-exp"> + </form>`, + idsToShowPopup: ["cc-number", "cc-name", "cc-exp"], + }, + { + description: "without @autocomplete - all fields in the same form", + document: `<form> + <input id="cc-number" placeholder="credit card number"> + <input id="cc-name" placeholder="credit card holder name"> + <input id="cc-exp" placeholder="expiration date"> + </form>`, + idsToShowPopup: ["cc-number", "cc-name", "cc-exp"], + }, + { + description: "@autocomplete - each field in its own form", + document: `<form><input id="cc-number" autocomplete="cc-number"></form> + <form><input id="cc-name" autocomplete="cc-name"></form> + <form><input id="cc-exp" autocomplete="cc-exp"></form>`, + idsToShowPopup: ["cc-number", "cc-name", "cc-exp"], + }, + { + description: + "without @autocomplete - each field in its own form (high-confidence cc-number & cc-name)", + document: `<form><input id="cc-number" placeholder="credit card number"></form> + <form><input id="cc-name" placeholder="credit card holder name"></form> + <form><input id="cc-exp" placeholder="expiration date"></form>`, + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold", + "0.9", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.95", + ], + ], + idsToShowPopup: ["cc-number", "cc-name"], + idsWithNoPopup: ["cc-exp"], + }, + { + description: + "without @autocomplete - each field in its own form (normal-confidence cc-number & cc-name)", + document: `<form><input id="cc-number" placeholder="credit card number"></form> + <form><input id="cc-name" placeholder="credit card holder name"></form> + <form><input id="cc-exp" placeholder="expiration date"></form>`, + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold", + "0.9", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.8", + ], + ], + idsWithNoPopup: ["cc-number", "cc-name", "cc-exp"], + }, + { + description: + "with @autocomplete - cc-number/cc-name and another <input> in a form", + document: `<form> + <input id="cc-number" autocomplete="cc-number"> + <input id="password" type="password"> + </form> + <form> + <input id="cc-name" autocomplete="cc-name"> + <input id="password" type="password"> + </form>`, + idsToShowPopup: ["cc-number", "cc-name"], + }, + { + description: + "without @autocomplete - high-confidence cc-number/cc-name and another <input> in a form", + document: `<form> + <input id="cc-number" placeholder="credit card number"> + <input id="password" type="password"> + </form> + <form> + <input id="cc-name" placeholder="credit card holder name"> + <input id="password" type="password"> + </form>`, + idsWithNoPopup: ["cc-number", "cc-name"], + }, + { + description: + "without @autocomplete - high-confidence cc-number/cc-name and another hidden <input> in a form", + document: `<form> + <input id="cc-number" placeholder="credit card number"> + <input id="token" type="hidden"> + </form> + <form> + <input id="cc-name" placeholder="credit card holder name"> + <input id="token" type="hidden"> + </form>`, + prefs: [ + [ + "extensions.formautofill.creditCards.heuristics.fathom.highConfidenceThreshold", + "0.9", + ], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "0.95", + ], + ], + idsToShowPopup: ["cc-number", "cc-name"], + }, +]; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.creditCards.supported", "on"], + ["extensions.formautofill.creditCards.enabled", true], + ], + }); + + await setStorage(TEST_CREDIT_CARD_1); +}); + +add_task(async function test_heuristics() { + for (const TEST of TESTCASES) { + info(`Test ${TEST.description}`); + if (TEST.prefs) { + await SpecialPowers.pushPrefEnv({ set: TEST.prefs }); + } + + await BrowserTestUtils.withNewTab(EMPTY_URL, async function (browser) { + await SpecialPowers.spawn(browser, [TEST.document], doc => { + // eslint-disable-next-line no-unsanitized/property + content.document.body.innerHTML = doc; + }); + + await SimpleTest.promiseFocus(browser); + + let ids = TEST.idsToShowPopup ?? []; + for (const id of ids) { + await runAndWaitForAutocompletePopupOpen(browser, async () => { + await focusAndWaitForFieldsIdentified(browser, `#${id}`); + }); + ok(true, `popup is opened on <input id=${id}>`); + } + + ids = TEST.idsWithNoPopup ?? []; + for (const id of ids) { + await focusAndWaitForFieldsIdentified(browser, `#${id}`); + await ensureNoAutocompletePopup(browser); + } + }); + + if (TEST.prefs) { + await SpecialPowers.popPrefEnv(); + } + } +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js new file mode 100644 index 0000000000..e3f12096c2 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_heuristics_cc_type.js @@ -0,0 +1,77 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const TEST_PROFILE = { + "cc-name": "John Doe", + "cc-number": "4111111111111111", + // "cc-type" should be remove from proile after fixing Bug 1834768. + "cc-type": "visa", + "cc-exp-month": 4, + "cc-exp-year": new Date().getFullYear(), +}; + +add_setup(async function () { + await SpecialPowers.pushPrefEnv({ + set: [ + ["extensions.formautofill.creditCards.supported", "on"], + ["extensions.formautofill.creditCards.enabled", true], + ], + }); +}); + +add_autofill_heuristic_tests([ + { + description: + "cc-type select does not have any information in labels or attributes", + fixtureData: ` + <form> + <input id="cc-number" autocomplete="cc-number"> + <input id="cc-name" autocomplete="cc-name"> + <select id="test"> + <option value="" selected="">0</option> + <option value="VISA">1</option> + <option value="MasterCard">2</option> + <option value="DINERS">3</option> + <option value="Discover">4</option> + </select> + </form> + <form> + <input id="cc-number" autocomplete="cc-number"> + <input id="cc-name" autocomplete="cc-name"> + <select id="test"> + <option value="0" selected="">Card Type</option> + <option value="1">Visa</option> + <option value="2">MasterCard</option> + <option value="3">Diners Club International</option> + <option value="4">Discover</option> + </select> + </form>`, + profile: TEST_PROFILE, + expectedResult: [ + { + description: "cc-type option.value has the hint", + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] }, + { fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] }, + { fieldName: "cc-type", reason: "regex-heuristic", autofill: "VISA" }, + ], + }, + { + description: "cc-type option.text has the hint", + default: { + reason: "autocomplete", + }, + fields: [ + { fieldName: "cc-number", autofill: TEST_PROFILE["cc-number"] }, + { fieldName: "cc-name", autofill: TEST_PROFILE["cc-name"] }, + { fieldName: "cc-type", reason: "regex-heuristic", autofill: "1" }, + ], + }, + ], + }, +]); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js new file mode 100644 index 0000000000..825a2978de --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_autodetect_type.js @@ -0,0 +1,104 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +add_task(async function test_autodetect_credit_not_set() { + const testCard = { + "cc-name": "John Doe", + "cc-number": "4012888888881881", + "cc-exp-month": "06", + "cc-exp-year": "2044", + }; + const expectedData = { + ...testCard, + ...{ "cc-type": "visa" }, + }; + let onChanged = waitForStorageChangedEvents("add"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let promiseShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": testCard["cc-name"], + "#cc-number": testCard["cc-number"], + "#cc-exp-month": testCard["cc-exp-month"], + "#cc-exp-year": testCard["cc-exp-year"], + "#cc-type": testCard["cc-type"], + }, + }); + + await promiseShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + + await onChanged; + + let creditCards = await getCreditCards(); + let savedCreditCard = creditCards[0]; + let decryptedNumber = await OSKeyStore.decrypt( + savedCreditCard["cc-number-encrypted"] + ); + savedCreditCard["cc-number"] = decryptedNumber; + for (let key in testCard) { + let expected = expectedData[key]; + let actual = savedCreditCard[key]; + Assert.equal(expected, actual, `${key} should match`); + } + await removeAllRecords(); +}); + +add_task(async function test_autodetect_credit_overwrite() { + const testCard = { + "cc-name": "John Doe", + "cc-number": "4012888888881881", + "cc-exp-month": "06", + "cc-exp-year": "2044", + "cc-type": "master", // Wrong credit card type + }; + const expectedData = { + ...testCard, + ...{ "cc-type": "visa" }, + }; + let onChanged = waitForStorageChangedEvents("add"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let promiseShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": testCard["cc-name"], + "#cc-number": testCard["cc-number"], + "#cc-exp-month": testCard["cc-exp-month"], + "#cc-exp-year": testCard["cc-exp-year"], + "#cc-type": testCard["cc-type"], + }, + }); + + await promiseShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + + await onChanged; + + let creditCards = await getCreditCards(); + let savedCreditCard = creditCards[0]; + let decryptedNumber = await OSKeyStore.decrypt( + savedCreditCard["cc-number-encrypted"] + ); + savedCreditCard["cc-number"] = decryptedNumber; + for (let key in testCard) { + let expected = expectedData[key]; + let actual = savedCreditCard[key]; + Assert.equal(expected, actual, `${key} should match`); + } + + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js new file mode 100644 index 0000000000..76e125a196 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_submission_normalized.js @@ -0,0 +1,109 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// We want to ensure that non-normalized credit card data is normalized +// correctly as part of the save credit card flow +add_task(async function test_new_submitted_card_is_normalized() { + const testCard = { + "cc-name": "Test User", + "cc-number": "5038146897157463", + "cc-exp-month": "4", + "cc-exp-year": "25", + }; + const expectedData = { + "cc-name": "Test User", + "cc-number": "5038146897157463", + "cc-exp-month": "4", + // cc-exp-year should be normalized to 2025 + "cc-exp-year": "2025", + }; + let onChanged = waitForStorageChangedEvents("add"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let promiseShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": testCard["cc-name"], + "#cc-number": testCard["cc-number"], + "#cc-exp-month": testCard["cc-exp-month"], + "#cc-exp-year": testCard["cc-exp-year"], + }, + }); + + await promiseShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + + await onChanged; + + let creditCards = await getCreditCards(); + let savedCreditCard = creditCards[0]; + let decryptedNumber = await OSKeyStore.decrypt( + savedCreditCard["cc-number-encrypted"] + ); + savedCreditCard["cc-number"] = decryptedNumber; + for (let key in testCard) { + let expected = expectedData[key]; + let actual = savedCreditCard[key]; + Assert.equal(expected, actual, `${key} should match`); + } + await removeAllRecords(); +}); + +add_task(async function test_updated_card_is_normalized() { + const testCard = { + "cc-name": "Test User", + "cc-number": "5038146897157463", + "cc-exp-month": "11", + "cc-exp-year": "20", + }; + await saveCreditCard(testCard); + const expectedData = { + "cc-name": "Test User", + "cc-number": "5038146897157463", + "cc-exp-month": "10", + // cc-exp-year should be normalized to 2027 + "cc-exp-year": "2027", + }; + let onChanged = waitForStorageChangedEvents("update"); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let promiseShown = waitForPopupShown(); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": testCard["cc-name"], + "#cc-number": testCard["cc-number"], + "#cc-exp-month": "10", + "#cc-exp-year": "27", + }, + }); + + await promiseShown; + await clickDoorhangerButton(MAIN_BUTTON); + } + ); + + await onChanged; + + let creditCards = await getCreditCards(); + let savedCreditCard = creditCards[0]; + savedCreditCard["cc-number"] = await OSKeyStore.decrypt( + savedCreditCard["cc-number-encrypted"] + ); + + for (let key in testCard) { + let expected = expectedData[key]; + let actual = savedCreditCard[key]; + Assert.equal(expected, actual, `${key} should match`); + } + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js new file mode 100644 index 0000000000..e8790243ac --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_creditCard_telemetry.js @@ -0,0 +1,872 @@ +"use strict"; + +const { TelemetryTestUtils } = ChromeUtils.importESModule( + "resource://testing-common/TelemetryTestUtils.sys.mjs" +); + +const CC_NUM_USES_HISTOGRAM = "CREDITCARD_NUM_USES"; + +function ccFormArgsv1(method, extra) { + return ["creditcard", method, "cc_form", undefined, extra]; +} + +function ccFormArgsv2(method, extra) { + return ["creditcard", method, "cc_form_v2", undefined, extra]; +} + +function buildccFormv2Extra(extra, defaultValue) { + let defaults = {}; + for (const field of [ + "cc_name", + "cc_number", + "cc_type", + "cc_exp", + "cc_exp_month", + "cc_exp_year", + ]) { + defaults[field] = defaultValue; + } + + return { ...defaults, ...extra }; +} + +async function assertTelemetry(expected_content, expected_parent) { + let snapshots; + + info( + `Waiting for ${expected_content?.length ?? 0} content events and ` + + `${expected_parent?.length ?? 0} parent events` + ); + + await TestUtils.waitForCondition( + () => { + snapshots = Services.telemetry.snapshotEvents( + Ci.nsITelemetry.DATASET_PRERELEASE_CHANNELS, + false + ); + + return ( + (snapshots.parent?.length ?? 0) >= (expected_parent?.length ?? 0) && + (snapshots.content?.length ?? 0) >= (expected_content?.length ?? 0) + ); + }, + "Wait for telemetry to be collected", + 100, + 100 + ); + + info(JSON.stringify(snapshots, null, 2)); + + if (expected_content !== undefined) { + expected_content = expected_content.map( + ([category, method, object, value, extra]) => { + return { category, method, object, value, extra }; + } + ); + + let clear = expected_parent === undefined; + + TelemetryTestUtils.assertEvents( + expected_content, + { + category: "creditcard", + }, + { clear, process: "content" } + ); + } + + if (expected_parent !== undefined) { + expected_parent = expected_parent.map( + ([category, method, object, value, extra]) => { + return { category, method, object, value, extra }; + } + ); + TelemetryTestUtils.assertEvents( + expected_parent, + { + category: "creditcard", + }, + { process: "parent" } + ); + } +} + +async function assertHistogram(histogramId, expectedNonZeroRanges) { + let actualNonZeroRanges = {}; + await TestUtils.waitForCondition( + () => { + const snapshot = Services.telemetry + .getHistogramById(histogramId) + .snapshot(); + // Compute the actual ranges in the format { range1: value1, range2: value2 }. + for (let [range, value] of Object.entries(snapshot.values)) { + if (value > 0) { + actualNonZeroRanges[range] = value; + } + } + + return ( + JSON.stringify(actualNonZeroRanges) == + JSON.stringify(expectedNonZeroRanges) + ); + }, + "Wait for telemetry to be collected", + 100, + 100 + ); + + Assert.equal( + JSON.stringify(actualNonZeroRanges), + JSON.stringify(expectedNonZeroRanges) + ); +} + +async function openTabAndUseCreditCard( + idx, + creditCard, + { closeTab = true, submitForm = true } = {} +) { + let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + + let tab = await BrowserTestUtils.openNewForegroundTab( + gBrowser, + CREDITCARD_FORM_URL + ); + let browser = tab.linkedBrowser; + + await openPopupOn(browser, "form #cc-name"); + for (let i = 0; i <= idx; i++) { + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + } + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await osKeyStoreLoginShown; + await waitForAutofill(browser, "#cc-number", creditCard["cc-number"]); + await focusUpdateSubmitForm( + browser, + { + focusSelector: "#cc-number", + newValues: {}, + }, + submitForm + ); + + if (!closeTab) { + return tab; + } + + await BrowserTestUtils.removeTab(tab); + return null; +} + +add_setup(async function () { + Services.telemetry.setEventRecordingEnabled("creditcard", true); + registerCleanupFunction(async function () { + Services.telemetry.setEventRecordingEnabled("creditcard", false); + }); +}); + +add_task(async function test_popup_opened() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ENABLED_AUTOFILL_CREDITCARDS_PREF, true], + [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"], + ], + }); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + await setStorage(TEST_CREDIT_CARD_1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + const focusInput = "#cc-number"; + + await openPopupOn(browser, focusInput); + + // Clean up + await closePopup(browser); + } + ); + + await assertTelemetry([ + ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")), + ccFormArgsv1("detected"), + ccFormArgsv2("popup_shown", { field_name: "cc-number" }), + ccFormArgsv1("popup_shown"), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + "formautofill.creditCards.detected_sections_count", + 1, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + "formautofill.creditCards.submitted_sections_count" + ); + + await removeAllRecords(); + await SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_popup_opened_form_without_autocomplete() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ENABLED_AUTOFILL_CREDITCARDS_PREF, true], + [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "1", + ], + ], + }); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + await setStorage(TEST_CREDIT_CARD_1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL }, + async function (browser) { + const focusInput = "#cc-number"; + + await openPopupOn(browser, focusInput); + + // Clean up + await closePopup(browser); + } + ); + + await assertTelemetry([ + ccFormArgsv2( + "detected", + buildccFormv2Extra({ cc_number: "1", cc_name: "1", cc_exp: "false" }, "0") + ), + ccFormArgsv1("detected"), + ccFormArgsv2("popup_shown", { field_name: "cc-number" }), + ccFormArgsv1("popup_shown"), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + "formautofill.creditCards.detected_sections_count", + 1, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + "formautofill.creditCards.submitted_sections_count" + ); + + await removeAllRecords(); + await SpecialPowers.popPrefEnv(); +}); + +add_task( + async function test_popup_opened_form_without_autocomplete_separate_cc_number() { + await SpecialPowers.pushPrefEnv({ + set: [ + [ENABLED_AUTOFILL_CREDITCARDS_PREF, true], + [AUTOFILL_CREDITCARDS_AVAILABLE_PREF, "on"], + [ + "extensions.formautofill.creditCards.heuristics.fathom.testConfidence", + "1", + ], + ], + }); + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + + await setStorage(TEST_CREDIT_CARD_1); + + // Click on the cc-number field of the form that only contains a cc-number field + // (detected by Fathom) + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL }, + async function (browser) { + await openPopupOn(browser, "#form2-cc-number #cc-number"); + await closePopup(browser); + } + ); + + await assertTelemetry([ + ccFormArgsv2("detected", buildccFormv2Extra({ cc_number: "1" }, "false")), + ccFormArgsv1("detected"), + ccFormArgsv2("popup_shown", { field_name: "cc-number" }), + ccFormArgsv1("popup_shown"), + ]); + + // Then click on the cc-name field of the form that doesn't have a cc-number field + // (detected by regexp-based heuristic) + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_WITHOUT_AUTOCOMPLETE_URL }, + async function (browser) { + await openPopupOn(browser, "#form2-cc-other #cc-name"); + await closePopup(browser); + } + ); + + await assertTelemetry([ + ccFormArgsv2( + "detected", + buildccFormv2Extra( + { cc_name: "1", cc_type: "0", cc_exp_month: "0", cc_exp_year: "0" }, + "false" + ) + ), + ccFormArgsv1("detected"), + ccFormArgsv2("popup_shown", { field_name: "cc-name" }), + ccFormArgsv1("popup_shown"), + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + "formautofill.creditCards.detected_sections_count", + 2, + "There should be 1 section detected." + ); + TelemetryTestUtils.assertScalarUnset( + TelemetryTestUtils.getProcessScalars("content"), + "formautofill.creditCards.submitted_sections_count" + ); + + await removeAllRecords(); + await SpecialPowers.popPrefEnv(); + } +); + +add_task(async function test_submit_creditCard_new() { + async function test_per_command( + command, + idx, + useCount = {}, + expectChanged = undefined + ) { + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]], + }); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let onChanged; + if (expectChanged !== undefined) { + onChanged = TestUtils.topicObserved("formautofill-storage-changed"); + } + + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-name": "User 1", + "#cc-number": "5038146897157463", + "#cc-exp-month": "12", + "#cc-exp-year": "2017", + "#cc-type": "mastercard", + }, + }); + + await onPopupShown; + await clickDoorhangerButton(command, idx); + if (expectChanged !== undefined) { + await onChanged; + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + "formautofill.creditCards.autofill_profiles_count", + expectChanged, + "There should be ${expectChanged} profile(s) stored." + ); + } + } + ); + + await assertHistogram(CC_NUM_USES_HISTOGRAM, useCount); + + await removeAllRecords(); + SpecialPowers.popPrefEnv(); + } + + Services.telemetry.clearEvents(); + Services.telemetry.clearScalars(); + Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear(); + + let expected_content = [ + ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")), + ccFormArgsv1("detected"), + ccFormArgsv2( + "submitted", + buildccFormv2Extra({ cc_exp: "unavailable" }, "user_filled") + ), + ccFormArgsv1("submitted", { + // 5 fields plus submit button + fields_not_auto: "6", + fields_auto: "0", + fields_modified: "0", + }), + ]; + await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1); + await assertTelemetry(expected_content, [ + ["creditcard", "show", "capture_doorhanger"], + ["creditcard", "save", "capture_doorhanger"], + ]); + + await test_per_command(SECONDARY_BUTTON); + await assertTelemetry(expected_content, [ + ["creditcard", "show", "capture_doorhanger"], + ["creditcard", "cancel", "capture_doorhanger"], + ]); + + await test_per_command(MENU_BUTTON, 0); + await assertTelemetry(expected_content, [ + ["creditcard", "show", "capture_doorhanger"], + ["creditcard", "disable", "capture_doorhanger"], + ]); + + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + "formautofill.creditCards.detected_sections_count", + 3, + "There should be 3 sections detected." + ); + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("content"), + "formautofill.creditCards.submitted_sections_count", + 3, + "There should be 1 section submitted." + ); +}); + +add_task(async function test_submit_creditCard_autofill() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + Services.telemetry.clearEvents(); + Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear(); + + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]], + }); + + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + Assert.equal(creditCards.length, 1, "1 credit card in storage"); + + await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1); + + await assertHistogram(CC_NUM_USES_HISTOGRAM, { + 1: 1, + }); + + SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF); + + await assertTelemetry( + [ + ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")), + ccFormArgsv1("detected"), + ccFormArgsv2("popup_shown", { field_name: "cc-name" }), + ccFormArgsv1("popup_shown"), + ccFormArgsv2( + "filled", + buildccFormv2Extra({ cc_exp: "unavailable" }, "filled") + ), + ccFormArgsv1("filled"), + ccFormArgsv2( + "submitted", + buildccFormv2Extra({ cc_exp: "unavailable" }, "autofilled") + ), + ccFormArgsv1("submitted", { + fields_not_auto: "3", + fields_auto: "5", + fields_modified: "0", + }), + ], + [] + ); + + await removeAllRecords(); + SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_submit_creditCard_update() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + async function test_per_command( + command, + idx, + useCount = {}, + expectChanged = undefined + ) { + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]], + }); + + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + Assert.equal(creditCards.length, 1, "1 credit card in storage"); + + let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(true); + await BrowserTestUtils.withNewTab( + { gBrowser, url: CREDITCARD_FORM_URL }, + async function (browser) { + let onPopupShown = waitForPopupShown(); + let onChanged; + if (expectChanged !== undefined) { + onChanged = TestUtils.topicObserved("formautofill-storage-changed"); + } + + await openPopupOn(browser, "form #cc-name"); + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + await osKeyStoreLoginShown; + + await waitForAutofill(browser, "#cc-name", "John Doe"); + await focusUpdateSubmitForm(browser, { + focusSelector: "#cc-name", + newValues: { + "#cc-exp-year": "2019", + }, + }); + await onPopupShown; + await clickDoorhangerButton(command, idx); + if (expectChanged !== undefined) { + await onChanged; + TelemetryTestUtils.assertScalar( + TelemetryTestUtils.getProcessScalars("parent"), + "formautofill.creditCards.autofill_profiles_count", + expectChanged, + "There should be ${expectChanged} profile(s) stored." + ); + } + } + ); + + await assertHistogram("CREDITCARD_NUM_USES", useCount); + + SpecialPowers.clearUserPref(ENABLED_AUTOFILL_CREDITCARDS_PREF); + + await removeAllRecords(); + } + Services.telemetry.clearEvents(); + Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear(); + + let expected_content = [ + ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")), + ccFormArgsv1("detected"), + ccFormArgsv2("popup_shown", { field_name: "cc-name" }), + ccFormArgsv1("popup_shown"), + ccFormArgsv2( + "filled", + buildccFormv2Extra({ cc_exp: "unavailable" }, "filled") + ), + ccFormArgsv1("filled"), + ccFormArgsv2("filled_modified", { field_name: "cc-exp-year" }), + ccFormArgsv1("filled_modified", { field_name: "cc-exp-year" }), + ccFormArgsv2( + "submitted", + buildccFormv2Extra( + { cc_exp: "unavailable", cc_exp_year: "user_filled" }, + "autofilled" + ) + ), + ccFormArgsv1("submitted", { + fields_not_auto: "3", + fields_auto: "5", + fields_modified: "1", + }), + ]; + + await test_per_command(MAIN_BUTTON, undefined, { 1: 1 }, 1); + await assertTelemetry(expected_content, [ + ["creditcard", "show", "update_doorhanger"], + ["creditcard", "update", "update_doorhanger"], + ]); + + await test_per_command(SECONDARY_BUTTON, undefined, { 0: 1, 1: 1 }, 2); + await assertTelemetry(expected_content, [ + ["creditcard", "show", "update_doorhanger"], + ["creditcard", "save", "update_doorhanger"], + ]); +}); + +const TEST_SELECTORS = { + selRecords: "#credit-cards", + btnRemove: "#remove", + btnAdd: "#add", + btnEdit: "#edit", +}; + +const DIALOG_SIZE = "width=600,height=400"; + +add_task(async function test_removingCreditCardsViaKeyboardDelete() { + Services.telemetry.clearEvents(); + + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]], + }); + + await setStorage(TEST_CREDIT_CARD_1); + let win = window.openDialog( + MANAGE_CREDIT_CARDS_DIALOG_URL, + null, + DIALOG_SIZE + ); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords); + + Assert.equal(selRecords.length, 1, "One credit card"); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + EventUtils.synthesizeKey("VK_DELETE", {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + Assert.equal(selRecords.length, 0, "No credit cards left"); + + win.close(); + + await assertTelemetry(undefined, [ + ["creditcard", "show", "manage"], + ["creditcard", "delete", "manage"], + ]); + + await removeAllRecords(); + SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_saveCreditCard() { + Services.telemetry.clearEvents(); + + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]], + }); + + await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-number"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey( + "0" + TEST_CREDIT_CARD_1["cc-exp-month"].toString(), + {}, + win + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey( + TEST_CREDIT_CARD_1["cc-exp-year"].toString(), + {}, + win + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + info("saving credit card"); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + }); + + await assertTelemetry(undefined, [["creditcard", "add", "manage"]]); + + await removeAllRecords(); + SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_editCreditCard() { + Services.telemetry.clearEvents(); + + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]], + }); + + await setStorage(TEST_CREDIT_CARD_1); + + let creditCards = await getCreditCards(); + Assert.equal(creditCards.length, 1, "only one credit card is in storage"); + await testDialog( + EDIT_CREDIT_CARD_DIALOG_URL, + win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("test", {}, win); + win.document.querySelector("#save").click(); + }, + { + record: creditCards[0], + } + ); + + await assertTelemetry(undefined, [ + ["creditcard", "show_entry", "manage"], + ["creditcard", "edit", "manage"], + ]); + + await removeAllRecords(); + SpecialPowers.popPrefEnv(); +}); + +add_task(async function test_histogram() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]], + }); + + Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear(); + + await setStorage( + TEST_CREDIT_CARD_1, + TEST_CREDIT_CARD_2, + TEST_CREDIT_CARD_3, + TEST_CREDIT_CARD_5 + ); + let creditCards = await getCreditCards(); + Assert.equal(creditCards.length, 4, "3 credit cards in storage"); + + await assertHistogram(CC_NUM_USES_HISTOGRAM, { + 0: 4, + }); + + await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1); + await assertHistogram(CC_NUM_USES_HISTOGRAM, { + 0: 3, + 1: 1, + }); + + await openTabAndUseCreditCard(1, TEST_CREDIT_CARD_2); + await assertHistogram(CC_NUM_USES_HISTOGRAM, { + 0: 2, + 1: 2, + }); + + await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_2); + await assertHistogram(CC_NUM_USES_HISTOGRAM, { + 0: 2, + 1: 1, + 2: 1, + }); + + await openTabAndUseCreditCard(1, TEST_CREDIT_CARD_1); + await assertHistogram(CC_NUM_USES_HISTOGRAM, { + 0: 2, + 2: 2, + }); + + await openTabAndUseCreditCard(2, TEST_CREDIT_CARD_5); + await assertHistogram(CC_NUM_USES_HISTOGRAM, { + 0: 1, + 1: 1, + 2: 2, + }); + + await removeAllRecords(); + SpecialPowers.popPrefEnv(); + + await assertHistogram(CC_NUM_USES_HISTOGRAM, {}); +}); + +add_task(async function test_clear_creditCard_autofill() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + Services.telemetry.clearEvents(); + Services.telemetry.getHistogramById(CC_NUM_USES_HISTOGRAM).clear(); + + await SpecialPowers.pushPrefEnv({ + set: [[ENABLED_AUTOFILL_CREDITCARDS_PREF, true]], + }); + + await setStorage(TEST_CREDIT_CARD_1); + let creditCards = await getCreditCards(); + Assert.equal(creditCards.length, 1, "1 credit card in storage"); + + let tab = await openTabAndUseCreditCard(0, TEST_CREDIT_CARD_1, { + closeTab: false, + submitForm: false, + }); + + let expected_content = [ + ccFormArgsv2("detected", buildccFormv2Extra({ cc_exp: "false" }, "true")), + ccFormArgsv1("detected"), + ccFormArgsv2("popup_shown", { field_name: "cc-name" }), + ccFormArgsv1("popup_shown"), + ccFormArgsv2( + "filled", + buildccFormv2Extra({ cc_exp: "unavailable" }, "filled") + ), + ccFormArgsv1("filled"), + ]; + await assertTelemetry(expected_content, []); + Services.telemetry.clearEvents(); + + let browser = tab.linkedBrowser; + + let popupshown = BrowserTestUtils.waitForPopupEvent( + browser.autoCompletePopup, + "shown" + ); + // Already focus in "cc-number" field, press 'down' to bring to popup. + await BrowserTestUtils.synthesizeKey("KEY_ArrowDown", {}, browser); + await popupshown; + + expected_content = [ + ccFormArgsv2("popup_shown", { field_name: "cc-number" }), + ccFormArgsv1("popup_shown"), + ]; + await assertTelemetry(expected_content, []); + Services.telemetry.clearEvents(); + + // kPress Clear Form. + await BrowserTestUtils.synthesizeKey("KEY_ArrowDown", {}, browser); + await BrowserTestUtils.synthesizeKey("KEY_Enter", {}, browser); + + expected_content = [ + ccFormArgsv2("filled_modified", { field_name: "cc-name" }), + ccFormArgsv1("filled_modified", { field_name: "cc-name" }), + ccFormArgsv2("filled_modified", { field_name: "cc-number" }), + ccFormArgsv1("filled_modified", { field_name: "cc-number" }), + ccFormArgsv2("filled_modified", { field_name: "cc-exp-month" }), + ccFormArgsv1("filled_modified", { field_name: "cc-exp-month" }), + ccFormArgsv2("filled_modified", { field_name: "cc-exp-year" }), + ccFormArgsv1("filled_modified", { field_name: "cc-exp-year" }), + ccFormArgsv2("filled_modified", { field_name: "cc-type" }), + ccFormArgsv1("filled_modified", { field_name: "cc-type" }), + ccFormArgsv2("cleared", { field_name: "cc-number" }), + // popup is shown again because when the field is cleared and is focused, + // we automatically triggers the popup. + ccFormArgsv2("popup_shown", { field_name: "cc-number" }), + ccFormArgsv1("popup_shown"), + ]; + + await assertTelemetry(expected_content, []); + Services.telemetry.clearEvents(); + + await BrowserTestUtils.removeTab(tab); + + await removeAllRecords(); + SpecialPowers.popPrefEnv(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js b/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js new file mode 100644 index 0000000000..f532ea5da9 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_editCreditCardDialog.js @@ -0,0 +1,422 @@ +"use strict"; + +add_setup(async function () { + let { formAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + ); + await formAutofillStorage.initialize(); +}); + +add_task(async function test_cancelEditCreditCardDialog() { + await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => { + win.document.querySelector("#cancel").click(); + }); +}); + +add_task(async function test_cancelEditCreditCardDialogWithESC() { + await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => { + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + }); +}); + +add_task(async function test_saveCreditCard() { + await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => { + ok( + win.document.documentElement + .querySelector("title") + .textContent.includes("Add"), + "Add card dialog title is correct" + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-number"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey( + "0" + TEST_CREDIT_CARD_1["cc-exp-month"].toString(), + {}, + win + ); + is( + win.document.activeElement.selectedOptions[0].text, + "04 - April", + "Displayed month should match number and name" + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey( + TEST_CREDIT_CARD_1["cc-exp-year"].toString(), + {}, + win + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_CREDIT_CARD_1["cc-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + info("saving credit card"); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + }); + let creditCards = await getCreditCards(); + + is(creditCards.length, 1, "only one credit card is in storage"); + for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD_1)) { + if (fieldName === "cc-number") { + fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4); + } + is(creditCards[0][fieldName], fieldValue, "check " + fieldName); + } + is(creditCards[0].billingAddressGUID, undefined, "check billingAddressGUID"); + ok(creditCards[0]["cc-number-encrypted"], "cc-number-encrypted exists"); +}); + +add_task(async function test_saveCreditCardWithMaxYear() { + await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-number"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey( + TEST_CREDIT_CARD_2["cc-exp-month"].toString(), + {}, + win + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey( + TEST_CREDIT_CARD_2["cc-exp-year"].toString(), + {}, + win + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_CREDIT_CARD_2["cc-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + info("saving credit card"); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + }); + let creditCards = await getCreditCards(); + + is(creditCards.length, 2, "Two credit cards are in storage"); + for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD_2)) { + if (fieldName === "cc-number") { + fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4); + } + is(creditCards[1][fieldName], fieldValue, "check " + fieldName); + } + ok(creditCards[1]["cc-number-encrypted"], "cc-number-encrypted exists"); + await removeCreditCards([creditCards[1].guid]); +}); + +add_task(async function test_saveCreditCardWithBillingAddress() { + await setStorage(TEST_ADDRESS_4, TEST_ADDRESS_1); + let addresses = await getAddresses(); + let billingAddress = addresses[0]; + + const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, { + billingAddressGUID: undefined, + }); + + await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-number"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey( + TEST_CREDIT_CARD["cc-exp-month"].toString(), + {}, + win + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey( + TEST_CREDIT_CARD["cc-exp-year"].toString(), + {}, + win + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(TEST_CREDIT_CARD["cc-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey(billingAddress["given-name"], {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + info("saving credit card"); + EventUtils.synthesizeKey("VK_RETURN", {}, win); + }); + let creditCards = await getCreditCards(); + + is(creditCards.length, 2, "Two credit cards are in storage"); + for (let [fieldName, fieldValue] of Object.entries(TEST_CREDIT_CARD)) { + if (fieldName === "cc-number") { + fieldValue = "*".repeat(fieldValue.length - 4) + fieldValue.substr(-4); + } + is(creditCards[1][fieldName], fieldValue, "check " + fieldName); + } + ok(creditCards[1]["cc-number-encrypted"], "cc-number-encrypted exists"); + await removeCreditCards([creditCards[1].guid]); + await removeAddresses([addresses[0].guid, addresses[1].guid]); +}); + +add_task(async function test_editCreditCard() { + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "only one credit card is in storage"); + await testDialog( + EDIT_CREDIT_CARD_DIALOG_URL, + win => { + ok( + win.document.documentElement + .querySelector("title") + .textContent.includes("Edit"), + "Edit card dialog title is correct" + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("test", {}, win); + win.document.querySelector("#save").click(); + }, + { + record: creditCards[0], + } + ); + ok(true, "Edit credit card dialog is closed"); + creditCards = await getCreditCards(); + + is(creditCards.length, 1, "only one credit card is in storage"); + is( + creditCards[0]["cc-name"], + TEST_CREDIT_CARD_1["cc-name"] + "test", + "cc name changed" + ); + await removeCreditCards([creditCards[0].guid]); + + creditCards = await getCreditCards(); + is(creditCards.length, 0, "Credit card storage is empty"); +}); + +add_task(async function test_editCreditCardWithMissingBillingAddress() { + const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, { + billingAddressGUID: "unknown-guid", + }); + await setStorage(TEST_CREDIT_CARD); + + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "one credit card in storage"); + is( + creditCards[0].billingAddressGUID, + TEST_CREDIT_CARD.billingAddressGUID, + "Check saved billingAddressGUID" + ); + await testDialog( + EDIT_CREDIT_CARD_DIALOG_URL, + win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_RIGHT", {}, win); + EventUtils.synthesizeKey("test", {}, win); + win.document.querySelector("#save").click(); + }, + { + record: creditCards[0], + } + ); + ok(true, "Edit credit card dialog is closed"); + creditCards = await getCreditCards(); + + is(creditCards.length, 1, "only one credit card is in storage"); + is( + creditCards[0]["cc-name"], + TEST_CREDIT_CARD["cc-name"] + "test", + "cc name changed" + ); + is( + creditCards[0].billingAddressGUID, + undefined, + "unknown GUID removed upon manual save" + ); + await removeCreditCards([creditCards[0].guid]); + + creditCards = await getCreditCards(); + is(creditCards.length, 0, "Credit card storage is empty"); +}); + +add_task(async function test_addInvalidCreditCard() { + await testDialog(EDIT_CREDIT_CARD_DIALOG_URL, async win => { + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("test", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("test name", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeMouseAtCenter( + win.document.querySelector("#save"), + {}, + win + ); + + is( + win.document.querySelector("form").checkValidity(), + false, + "cc-number is invalid" + ); + await ensureCreditCardDialogNotClosed(win); + info("closing"); + win.close(); + }); + info("closed"); + let creditCards = await getCreditCards(); + + is(creditCards.length, 0, "Credit card storage is empty"); +}); + +add_task(async function test_editInvalidCreditCardNumber() { + await setStorage(TEST_ADDRESS_4); + let addresses = await getAddresses(); + let billingAddress = addresses[0]; + + const INVALID_CREDIT_CARD_NUMBER = "123456789"; + const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_2, { + billingAddressGUID: billingAddress.guid, + guid: "invalid-number", + version: 2, + "cc-number": INVALID_CREDIT_CARD_NUMBER, + }); + + // Directly use FormAutofillStorage so we can set + // sourceSync: true, since saveCreditCard uses FormAutofillParent + // which doesn't expose this option. + let { formAutofillStorage } = ChromeUtils.importESModule( + "resource://autofill/FormAutofillStorage.sys.mjs" + ); + await formAutofillStorage.initialize(); + // Use `sourceSync: true` to bypass field normalization which will + // fail due to the invalid credit card number. + await formAutofillStorage.creditCards.add(TEST_CREDIT_CARD, { + sourceSync: true, + }); + + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "only one credit card is in storage"); + is( + creditCards[0]["cc-number"], + "*********", + "invalid credit card number stored" + ); + await testDialog( + EDIT_CREDIT_CARD_DIALOG_URL, + win => { + is( + win.document.querySelector("#cc-number").value, + INVALID_CREDIT_CARD_NUMBER, + "cc-number field should be showing invalid credit card number" + ); + is( + win.document.querySelector("#cc-number").checkValidity(), + false, + "cc-number is invalid" + ); + win.document.querySelector("#cancel").click(); + }, + { + record: creditCards[0], + skipDecryption: true, + } + ); + ok(true, "Edit credit card dialog is closed"); + creditCards = await getCreditCards(); + + is(creditCards.length, 1, "only one credit card is in storage"); + is( + creditCards[0]["cc-number"], + "*********", + "invalid cc number still in record" + ); + await removeCreditCards([creditCards[0].guid]); + await removeAddresses([addresses[0].guid]); + + creditCards = await getCreditCards(); + is(creditCards.length, 0, "Credit card storage is empty"); + addresses = await getAddresses(); + is(addresses.length, 0, "Address storage is empty"); +}); + +add_task(async function test_editCreditCardWithInvalidNumber() { + const TEST_CREDIT_CARD = Object.assign({}, TEST_CREDIT_CARD_1); + await setStorage(TEST_CREDIT_CARD); + + let creditCards = await getCreditCards(); + is(creditCards.length, 1, "only one credit card is in storage"); + await testDialog( + EDIT_CREDIT_CARD_DIALOG_URL, + win => { + ok( + win.document.documentElement + .querySelector("title") + .textContent.includes("Edit"), + "Edit card dialog title is correct" + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + is( + win.document.querySelector("#cc-number").validity.customError, + false, + "cc-number field should not have a custom error" + ); + EventUtils.synthesizeKey("4111111111111112", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + is( + win.document.querySelector("#cc-number").validity.customError, + true, + "cc-number field should have a custom error" + ); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + EventUtils.synthesizeKey("VK_TAB", {}, win); + win.document.querySelector("#cancel").click(); + }, + { + record: creditCards[0], + } + ); + ok(true, "Edit credit card dialog is closed"); + creditCards = await getCreditCards(); + + is(creditCards.length, 1, "only one credit card is in storage"); + await removeCreditCards([creditCards[0].guid]); + + creditCards = await getCreditCards(); + is(creditCards.length, 0, "Credit card storage is empty"); +}); + +add_task(async function test_noAutocompletePopupOnSystemTab() { + await setStorage(TEST_CREDIT_CARD_1); + + await BrowserTestUtils.withNewTab( + { gBrowser, url: PRIVACY_PREF_URL }, + async browser => { + // Open credit card manage dialog + await SpecialPowers.spawn(browser, [], async () => { + let button = content.document.querySelector( + "#creditCardAutofill button" + ); + button.click(); + }); + let dialog = await waitForSubDialogLoad( + content, + MANAGE_CREDIT_CARDS_DIALOG_URL + ); + + // Open edit credit card dialog + await SpecialPowers.spawn(dialog, [], async () => { + let button = content.document.querySelector("#add"); + button.click(); + }); + dialog = await waitForSubDialogLoad(content, EDIT_CREDIT_CARD_DIALOG_URL); + + // Focus on credit card number field + await SpecialPowers.spawn(dialog, [], async () => { + let number = content.document.querySelector("#cc-number"); + number.focus(); + }); + + // autocomplete popup should not appear + await ensureNoAutocompletePopup(browser); + } + ); + + await removeAllRecords(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js b/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js new file mode 100644 index 0000000000..5de499b942 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_insecure_form.js @@ -0,0 +1,145 @@ +"use strict"; + +// Remove the scheme from the URLs so we can switch between http: and https: later. +const TEST_URL_PATH_CC = + "://example.org" + + HTTP_TEST_PATH + + "creditCard/autocomplete_creditcard_basic.html"; +const TEST_URL_PATH = + "://example.org" + HTTP_TEST_PATH + "autocomplete_basic.html"; + +add_task(async function setup_storage() { + await setStorage( + TEST_ADDRESS_1, + TEST_ADDRESS_2, + TEST_ADDRESS_3, + TEST_CREDIT_CARD_1, + TEST_CREDIT_CARD_2, + TEST_CREDIT_CARD_3 + ); +}); + +add_task(async function test_insecure_form() { + async function runTest({ + urlPath, + protocol, + focusInput, + expectedType, + expectedResultLength, + }) { + await BrowserTestUtils.withNewTab( + { gBrowser, url: protocol + urlPath }, + async function (browser) { + await openPopupOn(browser, focusInput); + + const items = getDisplayedPopupItems(browser); + is( + items.length, + expectedResultLength, + `Should show correct amount of results in "${protocol}"` + ); + const firstItem = items[0]; + is( + firstItem.getAttribute("originaltype"), + expectedType, + `Item should attach with correct binding in "${protocol}"` + ); + + await closePopup(browser); + } + ); + } + + const testSets = [ + { + urlPath: TEST_URL_PATH, + protocol: "https", + focusInput: "#organization", + expectedType: "autofill-profile", + expectedResultLength: 2, + }, + { + urlPath: TEST_URL_PATH, + protocol: "http", + focusInput: "#organization", + expectedType: "autofill-profile", + expectedResultLength: 2, + }, + { + urlPath: TEST_URL_PATH_CC, + protocol: "https", + focusInput: "#cc-name", + expectedType: "autofill-profile", + expectedResultLength: 3, + }, + { + urlPath: TEST_URL_PATH_CC, + protocol: "http", + focusInput: "#cc-name", + expectedType: "autofill-insecureWarning", // insecure warning field + expectedResultLength: 1, + }, + ]; + + for (const test of testSets) { + await runTest(test); + } +}); + +add_task(async function test_click_on_insecure_warning() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "http" + TEST_URL_PATH_CC }, + async function (browser) { + await openPopupOn(browser, "#cc-name"); + const insecureItem = getDisplayedPopupItems(browser)[0]; + let popupClosePromise = BrowserTestUtils.waitForPopupEvent( + browser.autoCompletePopup, + "hidden" + ); + await EventUtils.synthesizeMouseAtCenter(insecureItem, {}); + // Check input's value after popup closed to ensure the completion of autofilling. + await popupClosePromise; + + const inputValue = await SpecialPowers.spawn( + browser, + [], + async function () { + return content.document.querySelector("#cc-name").value; + } + ); + is(inputValue, ""); + + await closePopup(browser); + } + ); +}); + +add_task(async function test_press_enter_on_insecure_warning() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: "http" + TEST_URL_PATH_CC }, + async function (browser) { + await openPopupOn(browser, "#cc-name"); + + await BrowserTestUtils.synthesizeKey("VK_DOWN", {}, browser); + + let popupClosePromise = BrowserTestUtils.waitForPopupEvent( + browser.autoCompletePopup, + "hidden" + ); + await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, browser); + // Check input's value after popup closed to ensure the completion of autofilling. + await popupClosePromise; + + const inputValue = await SpecialPowers.spawn( + browser, + [], + async function () { + return content.document.querySelector("#cc-name").value; + } + ); + is(inputValue, ""); + + await closePopup(browser); + } + ); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js b/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js new file mode 100644 index 0000000000..7f54140c78 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/browser_manageCreditCardsDialog.js @@ -0,0 +1,290 @@ +"use strict"; + +const TEST_SELECTORS = { + selRecords: "#credit-cards", + btnRemove: "#remove", + btnAdd: "#add", + btnEdit: "#edit", +}; + +const DIALOG_SIZE = "width=600,height=400"; + +add_task(async function test_manageCreditCardsInitialState() { + await BrowserTestUtils.withNewTab( + { gBrowser, url: MANAGE_CREDIT_CARDS_DIALOG_URL }, + async function (browser) { + await SpecialPowers.spawn(browser, [TEST_SELECTORS], args => { + let selRecords = content.document.querySelector(args.selRecords); + let btnRemove = content.document.querySelector(args.btnRemove); + let btnAdd = content.document.querySelector(args.btnAdd); + let btnEdit = content.document.querySelector(args.btnEdit); + + is(selRecords.length, 0, "No credit card"); + is(btnRemove.disabled, true, "Remove button disabled"); + is(btnAdd.disabled, false, "Add button enabled"); + is(btnEdit.disabled, true, "Edit button disabled"); + }); + } + ); +}); + +add_task(async function test_cancelManageCreditCardsDialogWithESC() { + let win = window.openDialog(MANAGE_CREDIT_CARDS_DIALOG_URL); + await waitForFocusAndFormReady(win); + let unloadPromise = BrowserTestUtils.waitForEvent(win, "unload"); + EventUtils.synthesizeKey("VK_ESCAPE", {}, win); + await unloadPromise; + ok(true, "Manage credit cards dialog is closed with ESC key"); +}); + +add_task(async function test_removingSingleAndMultipleCreditCards() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.reduceTimerPrecision", false]], + }); + await setStorage( + TEST_CREDIT_CARD_1, + TEST_CREDIT_CARD_2, + TEST_CREDIT_CARD_3, + TEST_CREDIT_CARD_4, + TEST_CREDIT_CARD_5 + ); + + let win = window.openDialog( + MANAGE_CREDIT_CARDS_DIALOG_URL, + null, + DIALOG_SIZE + ); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords); + let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove); + let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit); + + const expectedLabels = [ + { + id: "credit-card-label-number-name-2", + args: { number: "**** 1881", name: "Chris P. Bacon", type: "Visa" }, + }, + { + id: "credit-card-label-number-2", + args: { number: "**** 5100", type: "MasterCard" }, + }, + { + id: "credit-card-label-number-expiration-2", + args: { + number: "**** 7870", + month: "1", + year: "2000", + type: "MasterCard", + }, + }, + { + id: "credit-card-label-number-name-expiration-2", + args: { + number: "**** 1045", + name: "Timothy Berners-Lee", + month: "12", + year: (new Date().getFullYear() + 10).toString(), + type: "Visa", + }, + }, + { + id: "credit-card-label-number-name-expiration-2", + args: { + number: "**** 1111", + name: "John Doe", + month: "4", + year: new Date().getFullYear().toString(), + type: "Visa", + }, + }, + ]; + + is( + selRecords.length, + expectedLabels.length, + "Correct number of credit cards" + ); + expectedLabels.forEach((expected, i) => { + const l10nAttrs = document.l10n.getAttributes(selRecords[i]); + is( + l10nAttrs.id, + expected.id, + `l10n id set for credit card ${expectedLabels.length - i}` + ); + Object.keys(expected.args).forEach(arg => { + is( + l10nAttrs.args[arg], + expected.args[arg], + `Set display ${arg} for credit card ${expectedLabels.length - i}` + ); + }); + }); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + is(btnRemove.disabled, false, "Remove button enabled"); + is(btnEdit.disabled, false, "Edit button enabled"); + EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + is(selRecords.length, 4, "Four credit cards left"); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + EventUtils.synthesizeMouseAtCenter( + selRecords.children[3], + { shiftKey: true }, + win + ); + is(btnEdit.disabled, true, "Edit button disabled when multi-select"); + + EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + is(selRecords.length, 0, "All credit cards are removed"); + + win.close(); +}); + +add_task(async function test_removingCreditCardsViaKeyboardDelete() { + await setStorage(TEST_CREDIT_CARD_1); + let win = window.openDialog( + MANAGE_CREDIT_CARDS_DIALOG_URL, + null, + DIALOG_SIZE + ); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords); + + is(selRecords.length, 1, "One credit card"); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + EventUtils.synthesizeKey("VK_DELETE", {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + is(selRecords.length, 0, "No credit cards left"); + + win.close(); +}); + +add_task(async function test_creditCardsDialogWatchesStorageChanges() { + let win = window.openDialog( + MANAGE_CREDIT_CARDS_DIALOG_URL, + null, + DIALOG_SIZE + ); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords); + + await setStorage(TEST_CREDIT_CARD_1); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded"); + is(selRecords.length, 1, "One credit card is shown"); + + await removeCreditCards([selRecords.options[0].value]); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded"); + is(selRecords.length, 0, "Credit card is removed"); + win.close(); +}); + +add_task(async function test_showCreditCardIcons() { + await SpecialPowers.pushPrefEnv({ + set: [["privacy.reduceTimerPrecision", false]], + }); + await setStorage(TEST_CREDIT_CARD_1); + await setStorage(TEST_CREDIT_CARD_3); + + let win = window.openDialog( + MANAGE_CREDIT_CARDS_DIALOG_URL, + null, + DIALOG_SIZE + ); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords); + + is( + selRecords.classList.contains("branded"), + AppConstants.MOZILLA_OFFICIAL, + "record picker has 'branded' class in an MOZILLA_OFFICIAL build" + ); + + let option0 = selRecords.options[0]; + let icon0Url = win.getComputedStyle(option0, "::before").backgroundImage; + let option1 = selRecords.options[1]; + let icon1Url = win.getComputedStyle(option1, "::before").backgroundImage; + + is( + option0.getAttribute("cc-type"), + "mastercard", + "Option has the expected cc-type" + ); + is( + option1.getAttribute("cc-type"), + "visa", + "Option has the expected cc-type" + ); + + if (AppConstants.MOZILLA_OFFICIAL) { + ok( + icon0Url.includes("icon-credit-card-generic.svg"), + "unknown network option ::before element has the generic icon as backgroundImage: " + + icon0Url + ); + ok( + icon1Url.includes("cc-logo-visa.svg"), + "visa option ::before element has the visa icon as backgroundImage " + + icon1Url + ); + } + + await removeCreditCards([option0.value, option1.value]); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsLoaded"); + is(selRecords.length, 0, "Credit card is removed"); + win.close(); +}); + +add_task(async function test_hasEditLoginPrompt() { + if (!OSKeyStoreTestUtils.canTestOSKeyStoreLogin()) { + todo( + OSKeyStoreTestUtils.canTestOSKeyStoreLogin(), + "Cannot test OS key store login on official builds." + ); + return; + } + + await setStorage(TEST_CREDIT_CARD_1); + + let win = window.openDialog( + MANAGE_CREDIT_CARDS_DIALOG_URL, + null, + DIALOG_SIZE + ); + await waitForFocusAndFormReady(win); + + let selRecords = win.document.querySelector(TEST_SELECTORS.selRecords); + let btnRemove = win.document.querySelector(TEST_SELECTORS.btnRemove); + let btnAdd = win.document.querySelector(TEST_SELECTORS.btnAdd); + let btnEdit = win.document.querySelector(TEST_SELECTORS.btnEdit); + + EventUtils.synthesizeMouseAtCenter(selRecords.children[0], {}, win); + + let osKeyStoreLoginShown = OSKeyStoreTestUtils.waitForOSKeyStoreLogin(); // cancel + EventUtils.synthesizeMouseAtCenter(btnEdit, {}, win); + await osKeyStoreLoginShown; + await new Promise(resolve => waitForFocus(resolve, win)); + await new Promise(resolve => executeSoon(resolve)); + + // Login is not required for removing credit cards. + EventUtils.synthesizeMouseAtCenter(btnRemove, {}, win); + await BrowserTestUtils.waitForEvent(selRecords, "RecordsRemoved"); + is(selRecords.length, 0, "Credit card is removed"); + + // gSubDialog.open should be called when trying to add a credit card, + // no OS login dialog is required. + window.gSubDialog = { + open: url => + is(url, EDIT_CREDIT_CARD_DIALOG_URL, "Edit credit card dialog is called"), + }; + EventUtils.synthesizeMouseAtCenter(btnAdd, {}, win); + delete window.gSubDialog; + + win.close(); +}); diff --git a/browser/extensions/formautofill/test/browser/creditCard/head_cc.js b/browser/extensions/formautofill/test/browser/creditCard/head_cc.js new file mode 100644 index 0000000000..42196e8422 --- /dev/null +++ b/browser/extensions/formautofill/test/browser/creditCard/head_cc.js @@ -0,0 +1 @@ +/* import-globals-from ../head.js */ |